import 'dart:async'; import 'package:flutter/material.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart'; import 'package:get/get.dart'; import 'package:f0ckapp/controller/settingscontroller.dart'; class VideoControlsOverlay extends StatefulWidget { final CachedVideoPlayerPlusController controller; final VoidCallback? onDoubleTap; const VideoControlsOverlay({ super.key, required this.controller, this.onDoubleTap, }); @override State createState() => _VideoControlsOverlayState(); } class _VideoControlsOverlayState extends State { final SettingsController _settingsController = Get.find(); Timer? _hideTimer; bool _controlsVisible = false; bool _isScrubbing = false; Duration _scrubbingStartPosition = Duration.zero; double _scrubbingStartDx = 0.0; Duration _scrubbingSeekPosition = Duration.zero; @override void initState() { super.initState(); widget.controller.addListener(_listener); } @override void dispose() { _hideTimer?.cancel(); widget.controller.removeListener(_listener); super.dispose(); } void _listener() { if (mounted) { setState(() {}); } } void _startHideTimer() { _hideTimer?.cancel(); _hideTimer = Timer(const Duration(seconds: 5), () { if (mounted) { setState(() => _controlsVisible = false); } }); } void _toggleControlsVisibility() { setState(() => _controlsVisible = !_controlsVisible); if (_controlsVisible) { _startHideTimer(); } } void _handlePlayPause() { widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); _startHideTimer(); } void _onHorizontalDragStart(DragStartDetails details) { if (!widget.controller.value.isInitialized || !_controlsVisible) return; setState(() { _isScrubbing = true; _scrubbingStartPosition = widget.controller.value.position; _scrubbingStartDx = details.globalPosition.dx; _scrubbingSeekPosition = widget.controller.value.position; }); _hideTimer?.cancel(); } void _onHorizontalDragUpdate(DragUpdateDetails details) { if (!_isScrubbing) return; final double delta = details.globalPosition.dx - _scrubbingStartDx; final int seekMillis = _scrubbingStartPosition.inMilliseconds + (delta * 300).toInt(); setState(() { final Duration duration = widget.controller.value.duration; final Duration seekDuration = Duration(milliseconds: seekMillis); final Duration clampedSeekDuration = seekDuration < Duration.zero ? Duration.zero : (seekDuration > duration ? duration : seekDuration); _scrubbingSeekPosition = clampedSeekDuration; }); } void _onHorizontalDragEnd(DragEndDetails details) { if (!_isScrubbing) return; widget.controller.seekTo(_scrubbingSeekPosition); setState(() => _isScrubbing = false); _startHideTimer(); } void _onHorizontalDragCancel() { if (!_isScrubbing) return; setState(() => _isScrubbing = false); _startHideTimer(); } String _formatDuration(Duration? duration) { if (duration == null) return '00:00'; String twoDigits(int n) => n.toString().padLeft(2, '0'); return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}"; } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: _toggleControlsVisibility, onHorizontalDragStart: _controlsVisible ? _onHorizontalDragStart : null, onHorizontalDragUpdate: _controlsVisible ? _onHorizontalDragUpdate : null, onHorizontalDragEnd: _controlsVisible ? _onHorizontalDragEnd : null, onHorizontalDragCancel: _controlsVisible ? _onHorizontalDragCancel : null, child: Stack( alignment: Alignment.center, children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), color: _controlsVisible && !_isScrubbing ? Colors.black.withValues(alpha: 0.5) : Colors.transparent, ), AnimatedOpacity( opacity: _isScrubbing ? 1.0 : 0.0, duration: const Duration(milliseconds: 200), child: _buildScrubbingIndicator(), ), AnimatedOpacity( opacity: _controlsVisible && !_isScrubbing ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Align( alignment: Alignment.bottomCenter, child: _buildBottomBar(), ), ), ], ), ); } Widget _buildScrubbingIndicator() { final Duration positionChange = _scrubbingSeekPosition - _scrubbingStartPosition; final String changeSign = positionChange.isNegative ? '-' : '+'; final String changeText = _formatDuration(positionChange.abs()); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(8), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _formatDuration(_scrubbingSeekPosition), style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), ), Text( '[$changeSign$changeText]', style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 16, ), ), ], ), ); } Widget _buildBottomBar() { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Row( children: [ IconButton( icon: Icon( widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow, color: Theme.of(context).colorScheme.primary, size: 20, ), onPressed: _handlePlayPause, constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), Text( _formatDuration(widget.controller.value.position), style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 12, ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: VideoProgressIndicator( widget.controller, allowScrubbing: true, colors: VideoProgressColors( playedColor: Theme.of(context).colorScheme.primary, backgroundColor: Colors.white.withValues(alpha: 0.3), bufferedColor: Colors.white.withValues(alpha: 0.6), ), ), ), ), Text( _formatDuration(widget.controller.value.duration), style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 12, ), ), const SizedBox(width: 8), Obx( () => IconButton( icon: Icon( _settingsController.muted.value ? Icons.volume_off : Icons.volume_up, color: Theme.of(context).colorScheme.primary, size: 20, ), onPressed: () { _settingsController.toggleMuted(); _startHideTimer(); }, constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), ), ], ), ); } }