From 2d3ae1b0e573cae83a29e265cf9ec60d7a794d95 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 10 Oct 2026 23:58:16 +0200 Subject: [PATCH] Block the player dismiss pan from starting on side sliders The window-level dismiss pan recognizes before SwiftUI's slider DragGesture sets isAdjustingPlayerSliders, so dragging volume or brightness often dismissed the player instead. Side sliders now publish their frames and the pan refuses to begin from touches inside them. --- .../Navigation/NavigationCoordinator.swift | 6 ++++++ Yattee/Views/Player/ExpandedPlayerWindow.swift | 11 +++++++++++ Yattee/Views/Player/PlayerControlsView.swift | 15 +++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index 71e69e19..cdd64f66 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -144,6 +144,12 @@ final class NavigationCoordinator { /// otherwise be cancelled and dismiss the player instead). var panelResizeGrabberFrame: CGRect = .zero + /// Volume/brightness side slider frames in screen coordinates, keyed by + /// edge (leading = true). Touches inside must not start the sheet dismiss + /// pan — the window pan recognizes before SwiftUI's slider DragGesture + /// sets `isAdjustingPlayerSliders`, so the flag alone loses the race. + var playerSideSliderFrames: [Bool: CGRect] = [:] + /// Corner layout: width of the trailing screen-edge strip reserved for /// the side column reveal swipe while the column is collapsed (0 when /// not collapsed) — the seek pan must not begin from inside it. diff --git a/Yattee/Views/Player/ExpandedPlayerWindow.swift b/Yattee/Views/Player/ExpandedPlayerWindow.swift index ba4d66f4..54ee9efc 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindow.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindow.swift @@ -175,6 +175,7 @@ private final class DragToDismissGestureHandler: NSObject, UIGestureRecognizerDe var getCornerBelowPanelFrame: (() -> CGRect)? /// Returns the panel resize grabber strip frame in screen coordinates (blocks dismiss pan) var getPanelResizeGrabberFrame: (() -> CGRect)? + var getPlayerSideSliderFrames: (() -> [CGRect])? /// Returns whether the portrait panel is currently visible (not hidden off-screen) var isPortraitPanelVisible: (() -> Bool)? /// Returns the progress bar frame in screen coordinates (for gesture conflict resolution) @@ -383,6 +384,13 @@ private final class DragToDismissGestureHandler: NSObject, UIGestureRecognizerDe return false } + // ...or when the touch starts on a slider: the flag above is set by + // SwiftUI's DragGesture, which fires after this pan recognizes + let sliderTouch = panGesture.location(in: nil) + if (getPlayerSideSliderFrames?() ?? []).contains(where: { $0.contains(sliderTouch) }) { + return false + } + // Don't begin dismiss gesture when dragging portrait panel if isPanelDragging?() == true { return false @@ -871,6 +879,9 @@ final class ExpandedPlayerWindowManager { handler.getPanelResizeGrabberFrame = { [weak self] in self?.appEnvironment?.navigationCoordinator.panelResizeGrabberFrame ?? .zero } + handler.getPlayerSideSliderFrames = { [weak self] in + self?.appEnvironment?.navigationCoordinator.playerSideSliderFrames.values.filter { !$0.isEmpty } ?? [] + } handler.isPortraitPanelVisible = { [weak self] in self?.appEnvironment?.navigationCoordinator.isPortraitPanelVisible ?? true } diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index 637175eb..a5fbb898 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -1443,6 +1443,21 @@ struct PlayerControlsView: View { .modifier(SideSliderBackgroundModifier(buttonBackground: buttonBackground, theme: theme)) .opacity(playerState.isControlsLocked ? 0.5 : 1.0) .allowsHitTesting(!playerState.isControlsLocked) + // Publish the slider frame (with slop) so the window-level dismiss + // pan never begins from a slider drag + .background { + GeometryReader { proxy in + let isActive = shouldShowControls && !playerState.isControlsLocked + let frame = isActive ? proxy.frame(in: .global).insetBy(dx: -12, dy: -12) : .zero + Color.clear + .onChange(of: frame, initial: true) { _, frame in + appEnvironment?.navigationCoordinator.playerSideSliderFrames[isLeading] = frame + } + .onDisappear { + appEnvironment?.navigationCoordinator.playerSideSliderFrames[isLeading] = nil + } + } + } } }