Main coves client
1import 'package:flutter/material.dart';
2import 'package:video_player/video_player.dart';
3
4import '../constants/app_colors.dart';
5
6/// Minimal video controls showing only a scrubber/progress bar
7///
8/// Always visible at the bottom of the video, positioned above
9/// the Android navigation bar using SafeArea.
10class MinimalVideoControls extends StatefulWidget {
11 const MinimalVideoControls({required this.controller, super.key});
12
13 final VideoPlayerController controller;
14
15 @override
16 State<MinimalVideoControls> createState() => _MinimalVideoControlsState();
17}
18
19class _MinimalVideoControlsState extends State<MinimalVideoControls> {
20 double _sliderValue = 0;
21 bool _isUserDragging = false;
22
23 @override
24 void initState() {
25 super.initState();
26 widget.controller.addListener(_updateSlider);
27 }
28
29 @override
30 void dispose() {
31 widget.controller.removeListener(_updateSlider);
32 super.dispose();
33 }
34
35 void _updateSlider() {
36 if (!_isUserDragging && mounted) {
37 final position =
38 widget.controller.value.position.inMilliseconds.toDouble();
39 final duration =
40 widget.controller.value.duration.inMilliseconds.toDouble();
41
42 if (duration > 0) {
43 setState(() {
44 _sliderValue = position / duration;
45 });
46 }
47 }
48 }
49
50 void _onSliderChanged(double value) {
51 setState(() {
52 _sliderValue = value;
53 });
54 }
55
56 void _onSliderChangeStart(double value) {
57 _isUserDragging = true;
58 }
59
60 void _onSliderChangeEnd(double value) {
61 _isUserDragging = false;
62 final duration = widget.controller.value.duration;
63 final position = duration * value;
64 widget.controller.seekTo(position);
65 }
66
67 String _formatDuration(Duration duration) {
68 final minutes = duration.inMinutes;
69 final seconds = duration.inSeconds % 60;
70 return '${minutes.toString().padLeft(1, '0')}:'
71 '${seconds.toString().padLeft(2, '0')}';
72 }
73
74 @override
75 Widget build(BuildContext context) {
76 final position = widget.controller.value.position;
77 final duration = widget.controller.value.duration;
78
79 return Container(
80 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
81 decoration: BoxDecoration(
82 gradient: LinearGradient(
83 begin: Alignment.bottomCenter,
84 end: Alignment.topCenter,
85 colors: [
86 Colors.black.withValues(alpha: 0.7),
87 Colors.black.withValues(alpha: 0),
88 ],
89 ),
90 ),
91 child: Column(
92 mainAxisSize: MainAxisSize.min,
93 children: [
94 // Scrubber slider
95 SliderTheme(
96 data: SliderThemeData(
97 trackHeight: 3,
98 thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
99 overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
100 activeTrackColor: AppColors.primary,
101 inactiveTrackColor: Colors.white.withValues(alpha: 0.3),
102 thumbColor: AppColors.primary,
103 overlayColor: AppColors.primary.withValues(alpha: 0.3),
104 ),
105 child: Slider(
106 value: _sliderValue.clamp(0, 1.0),
107 onChanged: _onSliderChanged,
108 onChangeStart: _onSliderChangeStart,
109 onChangeEnd: _onSliderChangeEnd,
110 ),
111 ),
112 // Time labels
113 Padding(
114 padding: const EdgeInsets.symmetric(horizontal: 8),
115 child: Row(
116 mainAxisAlignment: MainAxisAlignment.spaceBetween,
117 children: [
118 Text(
119 _formatDuration(position),
120 style: const TextStyle(color: Colors.white, fontSize: 12),
121 ),
122 Text(
123 _formatDuration(duration),
124 style: TextStyle(
125 color: Colors.white.withValues(alpha: 0.7),
126 fontSize: 12,
127 ),
128 ),
129 ],
130 ),
131 ),
132 ],
133 ),
134 );
135 }
136}