at main 4.0 kB view raw
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}