Skip to content

Commit 072cbd8

Browse files
authored
Merge pull request #469 from OpenPecha/chore/RelatedResources
feat: implement YouTube video player with playback controls and integ…
2 parents a5970a7 + 2317d50 commit 072cbd8

12 files changed

Lines changed: 846 additions & 109 deletions

File tree

lib/features/home/presentation/widgets/youtube_video_player.dart

Lines changed: 186 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter/services.dart';
35
import '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';
57
import '../../../../shared/widgets/reusable_youtube_player.dart';
68

79
class YoutubeVideoPlayer extends StatefulWidget {
@@ -19,40 +21,179 @@ class YoutubeVideoPlayer extends StatefulWidget {
1921
}
2022

2123
class _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

Comments
 (0)