262 lines
7.9 KiB
Dart
262 lines
7.9 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;
|
|
|
|
const VideoControlsOverlay({super.key, required this.controller});
|
|
|
|
@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: IgnorePointer(
|
|
ignoring: !_controlsVisible,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(
|
|
widget.controller.value.isPlaying
|
|
? Icons.pause
|
|
: Icons.play_arrow,
|
|
size: 64,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
onPressed: _handlePlayPause,
|
|
),
|
|
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: [
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|