From 24a728e6921dea63ef8dc5f2784f21a92817532b Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 15 Apr 2026 03:33:09 +0200 Subject: [PATCH] Cancel tvOS scrub with Menu button instead of seeking Pressing Menu while scrubbing now discards the pending scrub and leaves playback time unchanged, instead of committing the seek via the focus-loss path. --- .../Player/tvOS/TVPlayerControlsView.swift | 6 ++++- .../Player/tvOS/TVPlayerProgressBar.swift | 27 +++++++++++++++++++ Yattee/Views/Player/tvOS/TVPlayerView.swift | 12 ++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Yattee/Views/Player/tvOS/TVPlayerControlsView.swift b/Yattee/Views/Player/tvOS/TVPlayerControlsView.swift index 6c037284..761a48b0 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerControlsView.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerControlsView.swift @@ -27,6 +27,9 @@ struct TVPlayerControlsView: View { var remoteSeekTime: TimeInterval? = nil /// Called when user presses left/right on the focused bar outside SELECT scrub. var onRemoteSeek: ((Bool) -> Void)? = nil + /// Bumped by the parent to cancel any in-progress scrub without seeking + /// (used when the Menu button is pressed while scrubbing). + var cancelScrubTrigger: UUID? = nil /// Whether the Debug button should be visible (user-toggled in Developer settings). private var showDebugButton: Bool { @@ -64,7 +67,8 @@ struct TVPlayerControlsView: View { isLive: playerState?.isLive ?? false, sponsorSegments: playerState?.sponsorSegments ?? [], remoteSeekTime: remoteSeekTime, - onRemoteSeek: onRemoteSeek + onRemoteSeek: onRemoteSeek, + cancelScrubTrigger: cancelScrubTrigger ) .focusSection() .padding(.horizontal, 88) diff --git a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift index 0793b09b..16001742 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift @@ -36,6 +36,9 @@ struct TVPlayerProgressBar: View { /// Called when the bar is focused (not scrubbing) and user presses left/right. /// Parameter: `forward` — true for right, false for left. var onRemoteSeek: ((Bool) -> Void)? = nil + /// Parent bumps this to request the bar to cancel any in-progress scrub + /// without performing a seek (used for the Menu button). + var cancelScrubTrigger: UUID? = nil /// Track focus state internally. @FocusState private var isFocused: Bool @@ -117,6 +120,10 @@ struct TVPlayerProgressBar: View { commitScrub() } } + .onChange(of: cancelScrubTrigger) { _, newValue in + guard newValue != nil, isScrubbing else { return } + cancelScrub() + } .animation(.easeInOut(duration: 0.2), value: isFocused) .animation(.easeInOut(duration: 0.1), value: isScrubbing) } @@ -389,6 +396,26 @@ struct TVPlayerProgressBar: View { } } + private func cancelScrub() { + seekTask?.cancel() + seekTask = nil + + let wasScrubbing = isScrubbing + + withAnimation(.easeOut(duration: 0.15)) { + scrubTime = nil + isScrubbing = false + } + panAccumulator = 0 + dpadStreakCount = 0 + lastDPadTime = nil + lastDPadDirection = nil + + if wasScrubbing { + onScrubbingChanged?(false) + } + } + } // MARK: - Pan Gesture View diff --git a/Yattee/Views/Player/tvOS/TVPlayerView.swift b/Yattee/Views/Player/tvOS/TVPlayerView.swift index c9683f92..6c6e6a77 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerView.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerView.swift @@ -85,6 +85,10 @@ struct TVPlayerView: View { /// last press. @State private var scrubberRemoteSeekTask: Task? + /// Bumped to signal `TVPlayerProgressBar` to cancel an in-progress scrub + /// without performing the seek (used when Menu is pressed during scrub). + @State private var cancelScrubTrigger: UUID? + // MARK: - Computed Properties private var playerService: PlayerService? { @@ -206,7 +210,8 @@ struct TVPlayerView: View { remoteSeekTime: scrubberRemoteSeekTime, onRemoteSeek: { forward in triggerScrubberRemoteSeek(forward: forward) - } + }, + cancelScrubTrigger: cancelScrubTrigger ) .transition(.opacity.animation(.easeInOut(duration: 0.25))) } @@ -623,8 +628,9 @@ struct TVPlayerView: View { // Third: hide details panel hideDetailsPanel() } else if isScrubbing { - // Fourth: exit scrub mode (handled by progress bar losing focus) - // Just hide controls + // Fourth: cancel scrub without seeking, then hide controls. The + // subsequent focus-loss path sees cleared scrub state and no-ops. + cancelScrubTrigger = UUID() hideControls() } else if controlsVisible { // Fifth: hide controls