import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; import '../../../core/constants/design_system.dart'; /// Lecteur vidéo plein écran avec contrôles. /// /// Affiche une vidéo en plein écran avec contrôles de lecture, /// barre de progression et gestion de l'orientation. class FullscreenVideoPlayer extends StatefulWidget { const FullscreenVideoPlayer({ required this.videoUrl, this.heroTag, this.title, super.key, }); final String videoUrl; final String? heroTag; final String? title; @override State createState() => _FullscreenVideoPlayerState(); /// Affiche le lecteur vidéo en plein écran. static Future show({ required BuildContext context, required String videoUrl, String? heroTag, String? title, }) { return Navigator.push( context, PageRouteBuilder( opaque: false, barrierColor: Colors.black, pageBuilder: (context, animation, secondaryAnimation) { return FadeTransition( opacity: animation, child: FullscreenVideoPlayer( videoUrl: videoUrl, heroTag: heroTag, title: title, ), ); }, ), ); } } class _FullscreenVideoPlayerState extends State { late VideoPlayerController _controller; bool _isInitialized = false; bool _showControls = true; bool _isPlaying = false; @override void initState() { super.initState(); _initializePlayer(); _setLandscapeOrientation(); } @override void dispose() { _resetOrientation(); _controller.dispose(); super.dispose(); } Future _initializePlayer() async { // Déterminer si c'est une URL réseau ou locale if (widget.videoUrl.startsWith('http')) { _controller = VideoPlayerController.networkUrl( Uri.parse(widget.videoUrl), ); } else { // Pour les fichiers locaux (pas encore implémenté) // _controller = VideoPlayerController.file(File(widget.videoUrl)); _controller = VideoPlayerController.networkUrl( Uri.parse(widget.videoUrl), ); } try { await _controller.initialize(); _controller.addListener(_videoListener); if (mounted) { setState(() { _isInitialized = true; }); // Démarrer la lecture automatiquement _controller.play(); _isPlaying = true; } } catch (e) { debugPrint('[FullscreenVideoPlayer] Erreur initialisation: $e'); } } void _videoListener() { if (_controller.value.isPlaying != _isPlaying) { setState(() { _isPlaying = _controller.value.isPlaying; }); } } Future _setLandscapeOrientation() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } Future _resetOrientation() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); await SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: SystemUiOverlay.values, ); } void _togglePlayPause() { setState(() { if (_controller.value.isPlaying) { _controller.pause(); } else { _controller.play(); } }); } void _toggleControls() { setState(() { _showControls = !_showControls; }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: GestureDetector( onTap: _toggleControls, child: Stack( fit: StackFit.expand, children: [ // Vidéo Center( child: _isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) : const CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ), // Contrôles if (_showControls) ...[ // Header _buildHeader(context), // Contrôles centraux _buildCenterControls(), // Footer avec barre de progression _buildFooter(), ], ], ), ), ); } Widget _buildHeader(BuildContext context) { return Positioned( top: 0, left: 0, right: 0, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: SafeArea( child: Padding( padding: const EdgeInsets.all(DesignSystem.spacingMd), child: Row( children: [ // Bouton retour IconButton( icon: const Icon( Icons.close_rounded, color: Colors.white, size: 28, ), onPressed: () => Navigator.of(context).pop(), ), const SizedBox(width: DesignSystem.spacingMd), // Titre if (widget.title != null && widget.title!.isNotEmpty) Expanded( child: Text( widget.title!, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ), ); } Widget _buildCenterControls() { return Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Reculer de 10s _buildControlButton( icon: Icons.replay_10_rounded, onTap: () { final currentPosition = _controller.value.position; final newPosition = currentPosition - const Duration(seconds: 10); _controller.seekTo( newPosition < Duration.zero ? Duration.zero : newPosition, ); }, ), const SizedBox(width: DesignSystem.spacingXl), // Play/Pause _buildControlButton( icon: _isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, onTap: _togglePlayPause, size: 80, ), const SizedBox(width: DesignSystem.spacingXl), // Avancer de 10s _buildControlButton( icon: Icons.forward_10_rounded, onTap: () { final currentPosition = _controller.value.position; final duration = _controller.value.duration; final newPosition = currentPosition + const Duration(seconds: 10); _controller.seekTo( newPosition > duration ? duration : newPosition, ); }, ), ], ), ); } Widget _buildControlButton({ required IconData icon, required VoidCallback onTap, double size = 60, }) { return GestureDetector( onTap: onTap, child: Container( padding: EdgeInsets.all(size / 5), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: Icon( icon, color: Colors.white, size: size / 1.5, ), ), ); } Widget _buildFooter() { if (!_isInitialized) return const SizedBox.shrink(); return Positioned( bottom: 0, left: 0, right: 0, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: SafeArea( child: Padding( padding: const EdgeInsets.all(DesignSystem.spacingMd), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Barre de progression VideoProgressIndicator( _controller, allowScrubbing: true, colors: const VideoProgressColors( playedColor: Colors.red, bufferedColor: Colors.white54, backgroundColor: Colors.white24, ), padding: EdgeInsets.zero, ), const SizedBox(height: DesignSystem.spacingSm), // Temps Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _formatDuration(_controller.value.position), style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), ), Text( _formatDuration(_controller.value.duration), style: const TextStyle( color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), ), ), ); } String _formatDuration(Duration duration) { final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); final seconds = duration.inSeconds.remainder(60); if (hours > 0) { return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } }