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