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 double screenWidth = MediaQuery.of(context).size.width; final bool isRewind = details.globalPosition.dx < screenWidth / 2; widget.onOverlayTap(); Future(() { if (isRewind) { final Duration newPosition = widget.controller.value.position - const Duration(seconds: 10); widget.controller.seekTo( newPosition < Duration.zero ? Duration.zero : newPosition, ); } else { final Duration newPosition = widget.controller.value.position + const Duration(seconds: 10); final Duration 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, ), ), ), ), IconButton( icon: Icon( widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow, size: 64, ), onPressed: () { widget.onOverlayTap(); widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); }, ), Positioned( right: 12, bottom: 12, child: IconButton( icon: Icon( widget.muted ? Icons.volume_off : Icons.volume_up, size: 16, ), onPressed: () { widget.onOverlayTap(); widget.onMuteToggle(); }, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 0), child: LayoutBuilder( builder: (context, constraints) { return 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: VideoProgressColors( playedColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0.5), bufferedColor: Theme.of( context, ).colorScheme.secondary.withValues(alpha: 0.5), ), ), ), if (widget.controller.value.duration.inMilliseconds > 0) Positioned( left: (widget.controller.value.position.inMilliseconds / widget .controller .value .duration .inMilliseconds) * constraints.maxWidth - 6, bottom: -4, child: Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: Theme.of(context).colorScheme.primary, border: Border.all( color: Theme.of(context).colorScheme.primary, 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)}"; } }