diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 3ef418d1..66f4bd64 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -200,6 +200,12 @@ extension ExpandedPlayerSheet { // Panel height when pinned (capped by maxPanelHeight for widescreen videos without description) let naturalPanelHeight = screenHeight - fitHeight let pinnedPanelHeight = min(naturalPanelHeight, maxPanelHeight) + let pillsOverlayOpacity: CGFloat = { + guard isPanelDragging, panelDragOffset > 0 else { return 1 } + let fadeDistance: CGFloat = 48 + let progress = min(1, panelDragOffset / fadeDistance) + return 1 - progress + }() // Video area height (space above panel) - may be larger than fitHeight when panel is capped let videoAreaHeight = screenHeight - pinnedPanelHeight @@ -413,6 +419,7 @@ extension ExpandedPlayerSheet { } : nil, playerControlsLayout: playerControlsLayout, onFullscreen: { [self] in toggleFullscreen() }, + pillsOverlayOpacity: pillsOverlayOpacity, onDragChanged: { [self] offset in // Set drag flags only on transition to avoid 120/sec @Observable writes if !isPanelDragging { diff --git a/Yattee/Views/Player/Gestures/OverscrollGestureView.swift b/Yattee/Views/Player/Gestures/OverscrollGestureView.swift index 543c79af..8a3c3248 100644 --- a/Yattee/Views/Player/Gestures/OverscrollGestureView.swift +++ b/Yattee/Views/Player/Gestures/OverscrollGestureView.swift @@ -13,6 +13,8 @@ import UIKit /// A transparent view that finds its parent UIScrollView and attaches an overscroll gesture handler. /// Use as a `.background` on a SwiftUI `ScrollView` to intercept pull-down gestures at scroll top. struct OverscrollGestureView: UIViewRepresentable { + /// Whether the handler should currently be attached. + var isEnabled: Bool = true /// Called during the drag with the vertical translation (positive = pulling down) var onDragChanged: ((CGFloat) -> Void)? /// Called when the drag ends with translation and predicted end translation @@ -30,6 +32,10 @@ struct OverscrollGestureView: UIViewRepresentable { context.coordinator.onDragChanged = onDragChanged context.coordinator.onDragEnded = onDragEnded + guard isEnabled else { + return view + } + // Schedule scroll view discovery after view is in hierarchy DispatchQueue.main.async { if let scrollView = Self.findScrollView(from: view) { @@ -45,6 +51,11 @@ struct OverscrollGestureView: UIViewRepresentable { context.coordinator.onDragChanged = onDragChanged context.coordinator.onDragEnded = onDragEnded + guard isEnabled else { + context.coordinator.gestureHandler.detach() + return + } + // If not attached yet, try again if context.coordinator.gestureHandler.scrollView == nil { DispatchQueue.main.async { diff --git a/Yattee/Views/Player/PortraitDetailsPanel.swift b/Yattee/Views/Player/PortraitDetailsPanel.swift index af1c0b0e..d5867aea 100644 --- a/Yattee/Views/Player/PortraitDetailsPanel.swift +++ b/Yattee/Views/Player/PortraitDetailsPanel.swift @@ -16,6 +16,7 @@ struct PortraitDetailsPanel: View { let onChannelTap: (() -> Void)? let playerControlsLayout: PlayerControlsLayout let onFullscreen: (() -> Void)? + let pillsOverlayOpacity: CGFloat // Drag gesture callbacks var onDragChanged: ((CGFloat) -> Void)? @@ -28,7 +29,6 @@ struct PortraitDetailsPanel: View { @State private var scrollToTopTrigger: Bool = false @State private var showQueueSheet: Bool = false @State private var showPlaylistSheet: Bool = false - @State private var panelHeight: CGFloat = 0 private var settingsManager: SettingsManager? { appEnvironment?.settingsManager } private var accentColor: Color { settingsManager?.accentColor.color ?? .accentColor } @@ -164,6 +164,7 @@ struct PortraitDetailsPanel: View { .background { // UIKit gesture handler for smooth overscroll-to-collapse OverscrollGestureView( + isEnabled: !isDraggingHandle, onDragChanged: { offset in onDragChanged?(offset) }, @@ -172,6 +173,7 @@ struct PortraitDetailsPanel: View { } ) } + .scrollDisabled(isDraggingHandle) .onScrollGeometryChange(for: CGFloat.self) { geometry in geometry.contentOffset.y } action: { _, newValue in @@ -238,6 +240,8 @@ struct PortraitDetailsPanel: View { .overlay { if !isCommentsExpanded { pillsOverlay + .opacity(pillsOverlayOpacity) + .allowsHitTesting(pillsOverlayOpacity > 0.01) } } // Expanded comments overlay @@ -343,12 +347,6 @@ struct PortraitDetailsPanel: View { .animation(isPanelDragging ? nil : .spring(response: 0.35, dampingFraction: 0.8), value: hasCommentsPill) .animation(isPanelDragging ? nil : .spring(response: 0.3, dampingFraction: 0.7), value: shouldShowPlayerPill) .animation(isPanelDragging ? nil : .easeInOut(duration: 0.2), value: isScrolled) - .onAppear { - panelHeight = geometry.size.height - } - .onChange(of: geometry.size.height) { _, newValue in - panelHeight = newValue - } } }