fix(player): require a fresh double tap for every mobile skip-zone seek
The skip badge doubled as an armed state: while it was up, any single tap in the same-direction zone seeked again. The badge is also raised by keyboard, D-pad, media-transport and live seeks, so one remote press armed one-tap seeking on the touch surface with no double tap at all. It stayed armed for 1200 ms plus the fade and renewed on every tap, leaving the side zones - 35% of the width each, over 70% of the height - unable to raise the chrome. Pair taps off the pending single-tap timer rather than differencing DateTime.now(). The window is then one deadline that a clock adjustment cannot stretch, suppressing touch taps disarms a half-finished pair, and _lastSkipTapTime belongs to the desktop double-click paths alone. Consecutive completed skips still accumulate into one running badge total.
This commit is contained in:
@@ -559,45 +559,41 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
|
||||
_lastSkipTapTime = now;
|
||||
}
|
||||
|
||||
/// Handle tap in skip zone with custom double-tap detection
|
||||
/// Handle a tap in a skip zone. Every skip costs a fresh same-direction
|
||||
/// double tap; a lone tap toggles the chrome.
|
||||
///
|
||||
/// The badge left over from the previous skip is a readout, not an armed
|
||||
/// state — it says nothing about what the next tap does.
|
||||
///
|
||||
/// The pending single-tap timer *is* the pairing window: while it is live the
|
||||
/// tap that started it is still unresolved, so a same-direction tap pairs with
|
||||
/// it. One deadline instead of a timer plus a `DateTime.now()` difference that
|
||||
/// a clock adjustment could stretch or collapse — and it leaves
|
||||
/// [_lastSkipTapTime] to the desktop double-click paths alone. Suppressing
|
||||
/// touch taps cancels the timer, which correctly disarms a half-finished pair.
|
||||
void _handleTapInSkipZone({required bool isForward}) {
|
||||
if (_isTouchTapSuppressed) return;
|
||||
|
||||
// Cancel any pending single-tap action
|
||||
final pairsWithPendingTap = (_singleTapTimer?.isActive ?? false) && _lastSkipTapWasForward == isForward;
|
||||
|
||||
// Either way the pending tap is resolved now: paired below, or replaced by
|
||||
// this one as the start of a new pair.
|
||||
_singleTapTimer?.cancel();
|
||||
_singleTapTimer = null;
|
||||
|
||||
// While the skip readout is visible, every tap in the same-direction zone
|
||||
// stacks another skip immediately — repeat skips cost one tap, not a
|
||||
// fresh double-tap. A tap in the opposite zone falls through to pairing.
|
||||
if (_showDoubleTapFeedback && _lastDoubleTapWasForward == isForward) {
|
||||
if (pairsWithPendingTap) {
|
||||
_handleDoubleTapSkip(isForward: isForward);
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final isDoubleTap =
|
||||
_lastSkipTapTime != null &&
|
||||
now.difference(_lastSkipTapTime!) < kDoubleTapTimeout &&
|
||||
_lastSkipTapWasForward == isForward;
|
||||
_lastSkipTapWasForward = isForward;
|
||||
|
||||
// Skip ONLY on detected double-tap (no single-tap-to-add behavior)
|
||||
if (isDoubleTap) {
|
||||
_lastSkipTapTime = null;
|
||||
_handleDoubleTapSkip(isForward: isForward);
|
||||
} else {
|
||||
// First tap - record timestamp and start timer for single-tap action
|
||||
_lastSkipTapTime = now;
|
||||
_lastSkipTapWasForward = isForward;
|
||||
|
||||
// If no second tap within the double-tap window, treat as single tap
|
||||
// to toggle controls
|
||||
_singleTapTimer = Timer(kDoubleTapTimeout, () {
|
||||
if (mounted) {
|
||||
_toggleControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
// No partner within the window, and this resolves as a lone tap.
|
||||
_singleTapTimer = Timer(kDoubleTapTimeout, () {
|
||||
if (mounted) {
|
||||
_toggleControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Size _sizeOf(BuildContext context) {
|
||||
@@ -613,7 +609,7 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
|
||||
_showSkipFeedback(isForward: isForward);
|
||||
}
|
||||
|
||||
/// Handle a skip-zone double tap (and every stacked tap that follows it).
|
||||
/// Handle a completed skip-zone double tap.
|
||||
void _handleDoubleTapSkip({required bool isForward}) {
|
||||
if (!widget.canControl) return;
|
||||
|
||||
|
||||
@@ -659,11 +659,15 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
bool _lastDoubleTapWasForward = true;
|
||||
Timer? _feedbackTimer;
|
||||
int _accumulatedSkipSeconds = 0; // Stacking skip: total skip during active feedback
|
||||
// Custom tap detection state (more reliable than Flutter's onDoubleTap)
|
||||
// Desktop double-click detection (more reliable than Flutter's onDoubleTap).
|
||||
// The mobile skip zones do not use this; they pair off _singleTapTimer.
|
||||
DateTime? _lastSkipTapTime;
|
||||
// Direction of the skip-zone tap _singleTapTimer is currently counting down.
|
||||
bool _lastSkipTapWasForward = true;
|
||||
Timer? _feedbackHideTimer; // Removes the skip readout after its fade-out completes
|
||||
Timer? _singleTapTimer; // Timer for delayed single-tap action (toggle controls)
|
||||
// Deferred lone-tap action for the skip zones, and the pairing window itself:
|
||||
// while it is active the tap that started it can still become a double tap.
|
||||
Timer? _singleTapTimer;
|
||||
final TwoFingerDoubleTapTracker _twoFingerDoubleTapTracker = TwoFingerDoubleTapTracker();
|
||||
final MobileEdgeAdjustmentTracker _edgeAdjustmentTracker = MobileEdgeAdjustmentTracker();
|
||||
final DeviceAdjustmentService _deviceAdjustmentService = DeviceAdjustmentService.instance;
|
||||
|
||||
Reference in New Issue
Block a user