v1.4.2+63
All checks were successful
Flutter Schmutter / build (push) Successful in 3m51s

This commit is contained in:
2025-06-21 16:28:57 +02:00
parent 73a44bb269
commit 7a88c23e57
10 changed files with 285 additions and 156 deletions

View File

@ -1,8 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
class VideoControlsOverlay extends StatelessWidget {
class VideoControlsOverlay extends StatefulWidget {
final CachedVideoPlayerPlusController controller;
final VoidCallback onOverlayTap;
final bool muted;
@ -16,51 +18,102 @@ class VideoControlsOverlay extends StatelessWidget {
required this.onMuteToggle,
});
@override
State<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
}
class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
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: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_ControlButton(Icons.replay_10, () {
onOverlayTap();
Duration newPosition =
controller.value.position - const Duration(seconds: 10);
if (newPosition < Duration.zero) newPosition = Duration.zero;
controller.seekTo(newPosition);
}),
const SizedBox(width: 40),
_ControlButton(
controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
() {
onOverlayTap();
controller.value.isPlaying
? controller.pause()
: controller.play();
},
size: 64,
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,
),
),
const SizedBox(width: 40),
_ControlButton(Icons.forward_10, () {
onOverlayTap();
Duration newPosition =
controller.value.position + const Duration(seconds: 10);
if (newPosition > controller.value.duration) {
newPosition = controller.value.duration;
}
controller.seekTo(newPosition);
}),
],
),
),
_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(muted ? Icons.volume_off : Icons.volume_up, () {
onOverlayTap();
onMuteToggle();
}, size: 16),
child: _ControlButton(
widget.muted ? Icons.volume_off : Icons.volume_up,
() {
widget.onOverlayTap();
widget.onMuteToggle();
},
size: 16,
),
),
Align(
alignment: Alignment.bottomCenter,
@ -73,16 +126,16 @@ class VideoControlsOverlay extends StatelessWidget {
left: 10,
bottom: 12,
child: Text(
'${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
'${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
Listener(
onPointerDown: (_) {
onOverlayTap();
widget.onOverlayTap();
},
child: VideoProgressIndicator(
controller,
widget.controller,
allowScrubbing: true,
padding: const EdgeInsets.only(top: 25.0),
colors: const VideoProgressColors(
@ -92,11 +145,15 @@ class VideoControlsOverlay extends StatelessWidget {
),
),
),
if (controller.value.duration.inMilliseconds > 0)
if (widget.controller.value.duration.inMilliseconds > 0)
Positioned(
left:
(controller.value.position.inMilliseconds /
controller.value.duration.inMilliseconds) *
(widget.controller.value.position.inMilliseconds /
widget
.controller
.value
.duration
.inMilliseconds) *
MediaQuery.of(context).size.width -
6,
bottom: -4,
@ -118,7 +175,8 @@ class VideoControlsOverlay extends StatelessWidget {
);
}
String _formatDuration(Duration duration) {
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)}";
}