@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -171,6 +172,11 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
LogicalKeyboardKey? _seekDirection; // Current direction being held
|
||||
int _seekRepeatCount = 0; // Consecutive key repeats for acceleration
|
||||
|
||||
// Preview thumbnail during sustained dpad/keyboard seeking
|
||||
bool _showKeyRepeatThumbnail = false;
|
||||
Timer? _keyRepeatThumbnailTimer;
|
||||
static const _keyRepeatThumbnailTimeout = Duration(milliseconds: 400);
|
||||
|
||||
// Content strip state
|
||||
bool _contentStripVisible = false;
|
||||
final GlobalKey<ContentStripState> _contentStripKey = GlobalKey<ContentStripState>();
|
||||
@@ -214,6 +220,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyRepeatThumbnailTimer?.cancel();
|
||||
_prevItemFocusNode.dispose();
|
||||
_prevChapterFocusNode.dispose();
|
||||
_skipBackFocusNode.dispose();
|
||||
@@ -429,6 +436,24 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
void _resetSeekState() {
|
||||
_seekDirection = null;
|
||||
_seekRepeatCount = 0;
|
||||
_keyRepeatThumbnailTimer?.cancel();
|
||||
_keyRepeatThumbnailTimer = null;
|
||||
if (_showKeyRepeatThumbnail) {
|
||||
setState(() => _showKeyRepeatThumbnail = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the timeline preview thumbnail during sustained key-repeat seeking.
|
||||
/// Arms a short timer that hides the thumbnail once repeats stop.
|
||||
void _triggerKeyRepeatThumbnail() {
|
||||
if (!_showKeyRepeatThumbnail) {
|
||||
setState(() => _showKeyRepeatThumbnail = true);
|
||||
}
|
||||
_keyRepeatThumbnailTimer?.cancel();
|
||||
_keyRepeatThumbnailTimer = Timer(_keyRepeatThumbnailTimeout, () {
|
||||
if (!mounted) return;
|
||||
setState(() => _showKeyRepeatThumbnail = false);
|
||||
});
|
||||
}
|
||||
|
||||
/// Calculate seek multiplier based on repeat count (stepped tiers)
|
||||
@@ -490,6 +515,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
}
|
||||
if (event is KeyRepeatEvent) {
|
||||
_seekRepeatCount++;
|
||||
_triggerKeyRepeatThumbnail();
|
||||
}
|
||||
|
||||
final isForward = key == LogicalKeyboardKey.arrowRight;
|
||||
@@ -680,6 +706,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
onFocusChange: _onFocusChange,
|
||||
enabled: canInteract,
|
||||
thumbnailDataBuilder: widget.thumbnailDataBuilder,
|
||||
showKeyRepeatThumbnail: _showKeyRepeatThumbnail,
|
||||
),
|
||||
],
|
||||
// Row 2: Playback controls and options
|
||||
|
||||
@@ -37,6 +37,11 @@ class TimelineSlider extends StatefulWidget {
|
||||
/// Optional callback that returns thumbnail image bytes for a given timestamp.
|
||||
final Uint8List? Function(Duration time)? thumbnailDataBuilder;
|
||||
|
||||
/// When true, show the preview thumbnail at the current playback position.
|
||||
/// Intended for sustained dpad/keyboard seeking where the decoder cannot
|
||||
/// keep up with accumulated seeks. Single presses should leave this false.
|
||||
final bool showKeyRepeatThumbnail;
|
||||
|
||||
const TimelineSlider({
|
||||
super.key,
|
||||
required this.position,
|
||||
@@ -51,6 +56,7 @@ class TimelineSlider extends StatefulWidget {
|
||||
this.onFocusChange,
|
||||
this.enabled = true,
|
||||
this.thumbnailDataBuilder,
|
||||
this.showKeyRepeatThumbnail = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -153,6 +159,13 @@ class _TimelineSliderState extends State<TimelineSlider> {
|
||||
final fraction = ((_mousePosition! - _sliderPadding) / trackWidth).clamp(0.0, 1.0);
|
||||
final time = Duration(milliseconds: (fraction * durationMs).round());
|
||||
tooltip = _buildTooltip(sliderWidth, _mousePosition!, time);
|
||||
} else if (widget.showKeyRepeatThumbnail && widget.thumbnailDataBuilder != null) {
|
||||
// Preview thumbnail at the current playback position while the
|
||||
// user holds a dpad/keyboard direction. The decoder lags behind
|
||||
// rapid seeks, so the BIF thumbnail is the only live feedback.
|
||||
final fraction = (widget.position.inMilliseconds / durationMs).clamp(0.0, 1.0);
|
||||
final px = _sliderPadding + fraction * trackWidth;
|
||||
tooltip = _buildTooltip(sliderWidth, px, widget.position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
/// Optional callback that returns thumbnail image bytes for a given timestamp.
|
||||
final Uint8List? Function(Duration time)? thumbnailDataBuilder;
|
||||
|
||||
/// When true, show the preview thumbnail at the current playback position
|
||||
/// (used during sustained dpad/keyboard key-repeat seeking).
|
||||
final bool showKeyRepeatThumbnail;
|
||||
|
||||
const VideoTimelineBar({
|
||||
super.key,
|
||||
required this.player,
|
||||
@@ -55,6 +59,7 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
this.enabled = true,
|
||||
this.showFinishTime = false,
|
||||
this.thumbnailDataBuilder,
|
||||
this.showKeyRepeatThumbnail = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -164,6 +169,7 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
onFocusChange: onFocusChange,
|
||||
enabled: enabled,
|
||||
thumbnailDataBuilder: thumbnailDataBuilder,
|
||||
showKeyRepeatThumbnail: showKeyRepeatThumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user