at main 4.9 kB view raw
1import 'package:flutter/foundation.dart'; 2import 'package:flutter/material.dart'; 3import 'package:video_player/video_player.dart'; 4 5import '../constants/app_colors.dart'; 6import 'minimal_video_controls.dart'; 7 8/// Fullscreen video player with swipe-to-dismiss gesture 9/// 10/// Displays the video player in fullscreen with a black background. 11/// Supports vertical swipe-down gesture to dismiss (like Instagram/TikTok). 12class FullscreenVideoPlayer extends StatefulWidget { 13 const FullscreenVideoPlayer({required this.videoUrl, super.key}); 14 15 final String videoUrl; 16 17 @override 18 State<FullscreenVideoPlayer> createState() => _FullscreenVideoPlayerState(); 19} 20 21class _FullscreenVideoPlayerState extends State<FullscreenVideoPlayer> 22 with WidgetsBindingObserver { 23 double _dragOffsetX = 0; 24 double _dragOffsetY = 0; 25 bool _isDragging = false; 26 VideoPlayerController? _videoController; 27 bool _isInitializing = true; 28 29 @override 30 void initState() { 31 super.initState(); 32 WidgetsBinding.instance.addObserver(this); 33 _initializePlayer(); 34 } 35 36 @override 37 void dispose() { 38 WidgetsBinding.instance.removeObserver(this); 39 _videoController?.dispose(); 40 super.dispose(); 41 } 42 43 @override 44 void didChangeAppLifecycleState(AppLifecycleState state) { 45 // Pause video when app goes to background 46 if (state == AppLifecycleState.paused || 47 state == AppLifecycleState.inactive) { 48 _videoController?.pause(); 49 } 50 } 51 52 Future<void> _initializePlayer() async { 53 try { 54 _videoController = VideoPlayerController.networkUrl( 55 Uri.parse(widget.videoUrl), 56 ); 57 58 await _videoController!.initialize(); 59 await _videoController!.play(); 60 61 if (mounted) { 62 setState(() { 63 _isInitializing = false; 64 }); 65 } 66 } on Exception catch (e) { 67 if (kDebugMode) { 68 debugPrint('Error initializing video: $e'); 69 } 70 if (mounted) { 71 setState(() { 72 _isInitializing = false; 73 }); 74 } 75 } 76 } 77 78 void _onPanUpdate(DragUpdateDetails details) { 79 setState(() { 80 _isDragging = true; 81 // Track both horizontal and vertical movement 82 _dragOffsetX += details.delta.dx; 83 _dragOffsetY += details.delta.dy; 84 }); 85 } 86 87 void _onPanEnd(DragEndDetails details) { 88 // If dragged more than 100 pixels vertically, dismiss 89 if (_dragOffsetY.abs() > 100) { 90 Navigator.of(context).pop(); 91 } else { 92 // Otherwise, animate back to original position 93 setState(() { 94 _dragOffsetX = 0.0; 95 _dragOffsetY = 0.0; 96 _isDragging = false; 97 }); 98 } 99 } 100 101 void _togglePlayPause() { 102 if (_videoController == null || !_videoController!.value.isInitialized) { 103 return; 104 } 105 106 setState(() { 107 if (_videoController!.value.isPlaying) { 108 _videoController!.pause(); 109 } else { 110 _videoController!.play(); 111 } 112 }); 113 } 114 115 @override 116 Widget build(BuildContext context) { 117 // Calculate opacity based on drag offset (fade out as user drags) 118 final opacity = (1.0 - (_dragOffsetY.abs() / 300)).clamp(0.0, 1.0); 119 120 return Scaffold( 121 backgroundColor: Colors.black.withValues(alpha: opacity), 122 body: GestureDetector( 123 onPanUpdate: _onPanUpdate, 124 onPanEnd: _onPanEnd, 125 onTap: _togglePlayPause, 126 child: Stack( 127 children: [ 128 // Video player - fills entire screen and moves with drag 129 AnimatedContainer( 130 duration: 131 _isDragging 132 ? Duration.zero 133 : const Duration(milliseconds: 200), 134 curve: Curves.easeOut, 135 transform: Matrix4.translationValues( 136 _dragOffsetX, 137 _dragOffsetY, 138 0, 139 ), 140 child: SizedBox.expand( 141 child: 142 _isInitializing || _videoController == null 143 ? const Center( 144 child: CircularProgressIndicator( 145 color: AppColors.loadingIndicator, 146 ), 147 ) 148 : Center( 149 child: AspectRatio( 150 aspectRatio: _videoController!.value.aspectRatio, 151 child: VideoPlayer(_videoController!), 152 ), 153 ), 154 ), 155 ), 156 // Minimal controls at bottom (scrubber only) 157 if (_videoController != null && 158 _videoController!.value.isInitialized) 159 Positioned( 160 bottom: 0, 161 left: 0, 162 right: 0, 163 child: SafeArea( 164 child: MinimalVideoControls(controller: _videoController!), 165 ), 166 ), 167 ], 168 ), 169 ), 170 ); 171 } 172}