Main coves client
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}