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