import 'dart:async'; import 'package:flutter/material.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart'; class VideoControlsOverlay extends StatefulWidget { final CachedVideoPlayerPlusController controller; final VoidCallback onOverlayTap; final bool muted; final VoidCallback onMuteToggle; const VideoControlsOverlay({ super.key, required this.controller, required this.onOverlayTap, required this.muted, required this.onMuteToggle, }); @override State createState() => _VideoControlsOverlayState(); } class _VideoControlsOverlayState extends State { bool _showSeekIndicator = false; bool _isRewinding = false; Timer? _hideTimer; @override void dispose() { _hideTimer?.cancel(); super.dispose(); } void _handleDoubleTap(TapDownDetails details) { final screenWidth = MediaQuery.of(context).size.width; final isRewind = details.globalPosition.dx < screenWidth / 2; Future(() { if (isRewind) { final newPosition = widget.controller.value.position - const Duration(seconds: 10); widget.controller.seekTo( newPosition < Duration.zero ? Duration.zero : newPosition, ); } else { final newPosition = widget.controller.value.position + const Duration(seconds: 10); final duration = widget.controller.value.duration; widget.controller.seekTo( newPosition > duration ? duration : newPosition, ); } }); _hideTimer?.cancel(); setState(() { _showSeekIndicator = true; _isRewinding = isRewind; }); _hideTimer = Timer(const Duration(milliseconds: 500), () { setState(() => _showSeekIndicator = false); }); } @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ GestureDetector( onTap: widget.onOverlayTap, onDoubleTapDown: _handleDoubleTap, child: Container(color: Colors.transparent), ), AnimatedOpacity( opacity: _showSeekIndicator ? 1.0 : 0.0, duration: const Duration(milliseconds: 200), child: Align( alignment: _isRewinding ? Alignment.centerLeft : Alignment.centerRight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40.0), child: Icon( _isRewinding ? Icons.fast_rewind_rounded : Icons.fast_forward_rounded, color: Colors.white70, size: 60, ), ), ), ), _ControlButton( widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow, () { widget.onOverlayTap(); widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); }, size: 64, ), Positioned( right: 12, bottom: 12, child: _ControlButton( widget.muted ? Icons.volume_off : Icons.volume_up, () { widget.onOverlayTap(); widget.onMuteToggle(); }, size: 16, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 0), child: Stack( clipBehavior: Clip.none, children: [ Positioned( left: 10, bottom: 12, child: Text( '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), Listener( onPointerDown: (_) { widget.onOverlayTap(); }, child: VideoProgressIndicator( widget.controller, allowScrubbing: true, padding: const EdgeInsets.only(top: 25.0), colors: const VideoProgressColors( playedColor: Colors.red, backgroundColor: Colors.grey, bufferedColor: Colors.white54, ), ), ), if (widget.controller.value.duration.inMilliseconds > 0) Positioned( left: (widget.controller.value.position.inMilliseconds / widget .controller .value .duration .inMilliseconds) * MediaQuery.of(context).size.width - 6, bottom: -4, child: Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.red, border: Border.all(color: Colors.red, width: 2), ), ), ), ], ), ), ), ], ); } 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)}"; } } class _ControlButton extends StatelessWidget { final IconData icon; final VoidCallback onPressed; final double size; const _ControlButton(this.icon, this.onPressed, {this.size = 24}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black.withValues(alpha: 0.4), ), child: IconButton( icon: Icon(icon, size: size), onPressed: onPressed, ), ); } }