Files
fApp/lib/widgets/video_controls_overlay.dart
2025-06-24 13:05:08 +02:00

261 lines
7.8 KiB
Dart

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<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
}
class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
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,
),
),
],
),
);
}
}