feat: timeline thumbnail on dpad key-repeat

close #805
This commit is contained in:
edde746
2026-04-19 12:24:14 +02:00
parent b4e78febc0
commit fd1d187ede
3 changed files with 46 additions and 0 deletions
@@ -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,
);
}
}