1+ import 'dart:async' ;
2+
13import 'package:flutter/material.dart' ;
24import 'package:flutter/services.dart' ;
35import 'package:flutter_pecha/core/constants/app_assets.dart' ;
4- import 'package:go_router/go_router .dart' ;
6+ import 'package:youtube_player_flutter/youtube_player_flutter .dart' ;
57import '../../../../shared/widgets/reusable_youtube_player.dart' ;
68
79class YoutubeVideoPlayer extends StatefulWidget {
@@ -19,40 +21,179 @@ class YoutubeVideoPlayer extends StatefulWidget {
1921}
2022
2123class _YoutubeVideoPlayerState extends State <YoutubeVideoPlayer > {
24+ YoutubePlayerController ? _controller;
25+ VoidCallback ? _stopPlayback;
26+ Timer ? _controlsTimer;
27+ bool _isPlaying = false ;
28+ bool _isReady = false ;
29+ bool _showPlaybackControls = true ;
30+
2231 @override
2332 void initState () {
2433 super .initState ();
2534 // Hide status bar and navigation bar for a true full-screen experience
2635 SystemChrome .setEnabledSystemUIMode (SystemUiMode .immersiveSticky);
36+ _scheduleControlsHide ();
2737 }
2838
2939 @override
3040 void dispose () {
31- // Restore system UI when leaving the player
41+ _controlsTimer? .cancel ();
42+ _stopPlayback? .call ();
43+ _controller = null ;
44+ _stopPlayback = null ;
45+ _restoreSystemUi ();
46+ super .dispose ();
47+ }
48+
49+ void _restoreSystemUi () {
3250 SystemChrome .setEnabledSystemUIMode (
3351 SystemUiMode .manual,
3452 overlays: SystemUiOverlay .values,
3553 );
36- super .dispose ();
54+ }
55+
56+ void _closePlayer () {
57+ _controlsTimer? .cancel ();
58+ _stopPlayback? .call ();
59+ _controller = null ;
60+ _stopPlayback = null ;
61+ _restoreSystemUi ();
62+ Navigator .of (context).pop ();
63+ }
64+
65+ void _seekBy (Duration offset) {
66+ final controller = _controller;
67+ if (controller == null || ! _isReady) return ;
68+
69+ final current = controller.value.position;
70+ final target = current + offset;
71+ controller.seekTo (target < Duration .zero ? Duration .zero : target);
72+ _showControlsTemporarily ();
73+ }
74+
75+ void _togglePlayPause () {
76+ final controller = _controller;
77+ if (controller == null || ! _isReady) return ;
78+
79+ HapticFeedback .selectionClick ();
80+ if (_isPlaying) {
81+ controller.pause ();
82+ } else {
83+ controller.play ();
84+ }
85+ setState (() => _isPlaying = ! _isPlaying);
86+ _showControlsTemporarily ();
87+ }
88+
89+ void _showControlsTemporarily () {
90+ if (! _showPlaybackControls) {
91+ setState (() => _showPlaybackControls = true );
92+ }
93+ _scheduleControlsHide ();
94+ }
95+
96+ void _scheduleControlsHide () {
97+ _controlsTimer? .cancel ();
98+ _controlsTimer = Timer (const Duration (seconds: 5 ), () {
99+ if (! mounted) return ;
100+ setState (() => _showPlaybackControls = false );
101+ });
37102 }
38103
39104 @override
40105 Widget build (BuildContext context) {
41- return Scaffold (
106+ return PopScope (
107+ onPopInvokedWithResult: (didPop, _) {
108+ if (didPop) {
109+ _controlsTimer? .cancel ();
110+ _stopPlayback? .call ();
111+ _controller = null ;
112+ _stopPlayback = null ;
113+ _restoreSystemUi ();
114+ }
115+ },
116+ child: Scaffold (
42117 backgroundColor: Colors .black,
43118 // Let the player go behind where the status bar was
44119 extendBodyBehindAppBar: true ,
45120 body: Stack (
46121 fit: StackFit .expand,
47122 children: [
48123 // ── Full-screen player ─────────────────────────────────────────
49- ReusableYoutubePlayer (
50- videoUrl: widget.videoUrl,
51- aspectRatio: 9 / 16 ,
52- autoPlay: true ,
53- mute: false ,
54- loop: true ,
55- fillParent: true ,
124+ AbsorbPointer (
125+ child: ReusableYoutubePlayer (
126+ videoUrl: widget.videoUrl,
127+ aspectRatio: 9 / 16 ,
128+ autoPlay: true ,
129+ mute: false ,
130+ loop: true ,
131+ fillParent: true ,
132+ onControllerCreated: (controller) => _controller = controller,
133+ onStopPlaybackRegistered: (stop) => _stopPlayback = stop,
134+ onReady: () {
135+ if (mounted) setState (() => _isReady = true );
136+ },
137+ onStateChanged: (isPlaying) {
138+ if (mounted) setState (() => _isPlaying = isPlaying);
139+ },
140+ ),
141+ ),
142+
143+ Positioned .fill (
144+ child: GestureDetector (
145+ behavior: HitTestBehavior .opaque,
146+ onTap: _showControlsTemporarily,
147+ ),
148+ ),
149+
150+ // ── Quick playback controls ────────────────────────────────────
151+ Center (
152+ child: IgnorePointer (
153+ ignoring: ! _showPlaybackControls,
154+ child: AnimatedOpacity (
155+ opacity: _showPlaybackControls ? 1 : 0 ,
156+ duration: const Duration (milliseconds: 180 ),
157+ child: Row (
158+ mainAxisSize: MainAxisSize .min,
159+ children: [
160+ _PlaybackControlButton (
161+ icon: Icons .replay_10_rounded,
162+ label: 'Back 10 seconds' ,
163+ onPressed:
164+ _isReady
165+ ? () {
166+ HapticFeedback .selectionClick ();
167+ _seekBy (const Duration (seconds: - 10 ));
168+ }
169+ : null ,
170+ ),
171+ const SizedBox (width: 20 ),
172+ _PlaybackControlButton (
173+ icon:
174+ _isPlaying
175+ ? Icons .pause_rounded
176+ : Icons .play_arrow_rounded,
177+ label: _isPlaying ? 'Pause' : 'Play' ,
178+ size: 45 ,
179+ onPressed: _isReady ? _togglePlayPause : null ,
180+ ),
181+ const SizedBox (width: 20 ),
182+ _PlaybackControlButton (
183+ icon: Icons .forward_10_rounded,
184+ label: 'Forward 10 seconds' ,
185+ onPressed:
186+ _isReady
187+ ? () {
188+ HapticFeedback .selectionClick ();
189+ _seekBy (const Duration (seconds: 10 ));
190+ }
191+ : null ,
192+ ),
193+ ],
194+ ),
195+ ),
196+ ),
56197 ),
57198
58199 // ── Back button overlay ────────────────────────────────────────
@@ -65,7 +206,7 @@ class _YoutubeVideoPlayerState extends State<YoutubeVideoPlayer> {
65206 backgroundColor: Colors .black45,
66207 shape: const CircleBorder (),
67208 ),
68- onPressed: () => context. pop () ,
209+ onPressed: _closePlayer ,
69210 ),
70211 ),
71212
@@ -89,6 +230,39 @@ class _YoutubeVideoPlayerState extends State<YoutubeVideoPlayer> {
89230 ),
90231 ],
91232 ),
233+ ),
234+ );
235+ }
236+ }
237+
238+ class _PlaybackControlButton extends StatelessWidget {
239+ const _PlaybackControlButton ({
240+ required this .icon,
241+ required this .label,
242+ required this .onPressed,
243+ this .size = 34 ,
244+ });
245+
246+ final IconData icon;
247+ final String label;
248+ final VoidCallback ? onPressed;
249+ final double size;
250+
251+ @override
252+ Widget build (BuildContext context) {
253+ return IconButton (
254+ tooltip: label,
255+ iconSize: size,
256+ color: Colors .white,
257+ disabledColor: Colors .white54,
258+ style: IconButton .styleFrom (
259+ backgroundColor: Colors .black45,
260+ disabledBackgroundColor: Colors .black26,
261+ shape: const CircleBorder (),
262+ padding: const EdgeInsets .all (10 ),
263+ ),
264+ onPressed: onPressed,
265+ icon: Icon (icon),
92266 );
93267 }
94268}
0 commit comments