Files
fApp/lib/widgets/video_controls_overlay.dart
Flummi 95f6dcfe2b
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
v1.4.4+65
2025-06-22 03:02:18 +02:00

203 lines
6.6 KiB
Dart

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<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 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)}";
}
}