From 3e5b38753270ba9e3b648b0a52100ce19a8bbeba Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 22 Sep 2026 23:53:32 +0200 Subject: [PATCH] Apply panel default visibility to portrait panel without position jump The preset's When Player Opens setting only drove the landscape/floating panel state, so on iPhone in portrait the details panel reappeared on every player expansion regardless of the setting. Mirror the resolved visibility into the portrait panel state, and seed it into the sheet's initial state at all creation sites so the first frame already renders the correct layout - applying it in onAppear animated a visible top-to-center video jump during expansion. Portrait panel toggles now also persist the last visibility so Remember Last works there too. --- Yattee/ContentView.swift | 6 +- .../Player/ExpandedPlayerSheet+Layouts.swift | 6 ++ Yattee/Views/Player/ExpandedPlayerSheet.swift | 67 +++++++++++++++++-- .../Views/Player/ExpandedPlayerWindow.swift | 8 ++- .../Player/ExpandedPlayerWindowManager.swift | 7 +- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index 0a335807..9c7e4c3b 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -64,7 +64,11 @@ struct ContentView: View { if isInlinePlayerOverlayActive(appEnvironment: appEnvironment) { ZStack { Color.black - ExpandedPlayerSheet() + ExpandedPlayerSheet( + initialPortraitPanelVisible: ExpandedPlayerSheet.initialPortraitPanelVisibility( + settingsManager: appEnvironment.settingsManager + ) + ) } .ignoresSafeArea() .transition(.opacity) diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 895a0da6..22a52d44 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -385,6 +385,7 @@ extension ExpandedPlayerSheet { panelRevealOffset = 0 isPortraitPanelVisible = true navigationCoordinator?.isPortraitPanelVisible = true + appEnvironment?.settingsManager.detailsPanelLastVisible = true // Match video position from reveal formula let revealProgress = min(1.0, -currentRevealOffset / pinnedPanelHeight) let maxVideoOffset = centerY - topY @@ -508,6 +509,7 @@ extension ExpandedPlayerSheet { withTransaction(transaction) { isPortraitPanelVisible = false navigationCoordinator?.isPortraitPanelVisible = false + appEnvironment?.settingsManager.detailsPanelLastVisible = false isCommentsExpanded = false navigationCoordinator?.isCommentsExpanded = false navigationCoordinator?.commentsFrame = .zero @@ -648,6 +650,7 @@ extension ExpandedPlayerSheet { isPortraitPanelVisible = newValue } navigationCoordinator?.isPortraitPanelVisible = newValue + appEnvironment?.settingsManager.detailsPanelLastVisible = newValue } /// Hide floating panel with animation @@ -656,6 +659,7 @@ extension ExpandedPlayerSheet { isPortraitPanelVisible = false } navigationCoordinator?.isPortraitPanelVisible = false + appEnvironment?.settingsManager.detailsPanelLastVisible = false // Reset main window transform when hiding panel to fix safe area clipping (iOS only) #if os(iOS) @@ -853,6 +857,7 @@ extension ExpandedPlayerSheet { isPortraitPanelVisible.toggle() } navigationCoordinator?.isPortraitPanelVisible = isPortraitPanelVisible + appEnvironment?.settingsManager.detailsPanelLastVisible = isPortraitPanelVisible }, onShowSettings: { [self] in showingQualitySheet = true @@ -1125,6 +1130,7 @@ extension ExpandedPlayerSheet { isPortraitPanelVisible.toggle() } navigationCoordinator?.isPortraitPanelVisible = isPortraitPanelVisible + appEnvironment?.settingsManager.detailsPanelLastVisible = isPortraitPanelVisible // Re-enable controls after animation completes (0.35s) // Controls will still be hidden (forceInitialHidden) until user taps DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 8623ae3f..eefe27bb 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -35,6 +35,26 @@ struct ExpandedPlayerSheet: View { /// Temporarily hide controls during fullscreen transition to avoid animation glitch @State var hideControlsDuringTransition: Bool = false + /// Resolves the preset's default panel visibility for the portrait panel + /// from the cached settings, so creators of this view can seed the initial + /// state BEFORE the first frame — applying it in onAppear animates a + /// visible top-to-center video jump during player expansion. + @MainActor + static func initialPortraitPanelVisibility(settingsManager: SettingsManager?) -> Bool { + switch PanelSettings.cached.effectiveDefaultVisibility { + case .hidden: + return false + case .visible: + return true + case .remember: + return settingsManager?.detailsPanelLastVisible ?? false + } + } + + init(initialPortraitPanelVisible: Bool = true) { + _isPortraitPanelVisible = State(initialValue: initialPortraitPanelVisible) + } + // MARK: - Panel Drag State /// Current drag offset when dismissing panel (positive = dragging down) @@ -194,6 +214,8 @@ struct ExpandedPlayerSheet: View { isAutoplayCancelled: $isAutoplayCancelled, pendingScrollToTopOnHeightChange: $pendingScrollToTopOnHeightChange, playerControlsLayout: $playerControlsLayout, + isPortraitPanelVisible: $isPortraitPanelVisible, + isInWideScreenLayout: isInWideScreenLayout, stopAutoplayCountdown: stopAutoplayCountdown, startAutoplayCountdown: startAutoplayCountdown, startPreloadingComments: startPreloadingComments, @@ -399,10 +421,23 @@ struct ExpandedPlayerSheet: View { // state from a previous session). Uses the cached settings; // loadPlayerControlsLayout re-applies the authoritative value. if navigationCoordinator?.hasAppliedPanelDefaultVisibility != true { - applyPanelDefaultVisibility( + let visible = applyPanelDefaultVisibility( PanelSettings.cached, settingsManager: appEnvironment?.settingsManager ) + #if os(iOS) || os(macOS) + LoggingService.shared.logPlayer("[ExpandedPlayerSheet] onAppear provisional apply: visible=\(visible), state=\(isPortraitPanelVisible), wideScreen=\(wideScreen)") + // No animation: this is the opening state, not a user + // toggle — animating it here slides the video mid-expand. + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + isPortraitPanelVisible = visible + } + if !wideScreen { + navigationCoordinator?.isPortraitPanelVisible = visible + } + #endif } } } @@ -794,6 +829,8 @@ private struct PlayerEventHandlersModifier: ViewModifier { @Binding var isAutoplayCancelled: Bool @Binding var pendingScrollToTopOnHeightChange: Bool @Binding var playerControlsLayout: PlayerControlsLayout + @Binding var isPortraitPanelVisible: Bool + let isInWideScreenLayout: Bool let stopAutoplayCountdown: () -> Void let startAutoplayCountdown: () -> Void @@ -914,10 +951,21 @@ private struct PlayerEventHandlersModifier: ViewModifier { // Apply the preset's default panel visibility once per expanded // player session, now that the authoritative settings are loaded. if navigationCoordinator?.hasAppliedPanelDefaultVisibility != true { - applyPanelDefaultVisibility( + let visible = applyPanelDefaultVisibility( layout.effectivePanelSettings, settingsManager: appEnvironment?.settingsManager ) + #if os(iOS) || os(macOS) + LoggingService.shared.logPlayer("[ExpandedPlayerSheet] authoritative apply: visible=\(visible), state=\(isPortraitPanelVisible), wideScreen=\(isInWideScreenLayout)") + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + isPortraitPanelVisible = visible + } + if !isInWideScreenLayout { + navigationCoordinator?.isPortraitPanelVisible = visible + } + #endif navigationCoordinator?.hasAppliedPanelDefaultVisibility = true } } @@ -926,18 +974,25 @@ private struct PlayerEventHandlersModifier: ViewModifier { /// Applies the preset's default details panel visibility for a fresh /// panel mount. "Hidden" mirrors the historical reset (unpinned too); -/// the other modes leave the pinned state as persisted. +/// the other modes leave the pinned state as persisted. Returns the +/// resolved visibility so callers can mirror it into the portrait +/// panel state, which lives in view @State rather than settings. @MainActor -private func applyPanelDefaultVisibility(_ settings: PanelSettings, settingsManager: SettingsManager?) { - guard let settingsManager else { return } +@discardableResult +private func applyPanelDefaultVisibility(_ settings: PanelSettings, settingsManager: SettingsManager?) -> Bool { + guard let settingsManager else { return true } switch settings.effectiveDefaultVisibility { case .hidden: settingsManager.landscapeDetailsPanelVisible = false settingsManager.landscapeDetailsPanelPinned = false + return false case .visible: settingsManager.landscapeDetailsPanelVisible = true + return true case .remember: - settingsManager.landscapeDetailsPanelVisible = settingsManager.detailsPanelLastVisible + let lastVisible = settingsManager.detailsPanelLastVisible + settingsManager.landscapeDetailsPanelVisible = lastVisible + return lastVisible } } diff --git a/Yattee/Views/Player/ExpandedPlayerWindow.swift b/Yattee/Views/Player/ExpandedPlayerWindow.swift index 26163bce..1ecdab40 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindow.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindow.swift @@ -769,7 +769,13 @@ final class ExpandedPlayerWindowManager { let statusBarController = StatusBarVisibilityController() // Create the expanded player view with status bar controller in environment - let expandedView = ExpandedPlayerSheet() + // Seed the portrait panel state before the first frame so the preset's + // default visibility doesn't animate a video position jump on expand. + let initialPanelVisible = ExpandedPlayerSheet.initialPortraitPanelVisibility( + settingsManager: appEnvironment.settingsManager + ) + LoggingService.shared.logPlayer("[ExpandedPlayerWindowManager] show: initialPortraitPanelVisible=\(initialPanelVisible), defaultVisibility=\(PanelSettings.cached.effectiveDefaultVisibility)") + let expandedView = ExpandedPlayerSheet(initialPortraitPanelVisible: initialPanelVisible) .appEnvironment(appEnvironment) .environment(\.statusBarVisibilityController, statusBarController) diff --git a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift index f0b2b758..fc615b89 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift @@ -850,6 +850,7 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate { /// its player window renders, and removes the perceptible "dead gap" between the /// click and the window appearing. private struct ExpandedPlayerWindowRoot: View { + @Environment(\.appEnvironment) private var appEnvironment @State private var showFullPlayer = false var body: some View { @@ -859,7 +860,11 @@ private struct ExpandedPlayerWindowRoot: View { Color.black if showFullPlayer { - ExpandedPlayerSheet() + ExpandedPlayerSheet( + initialPortraitPanelVisible: ExpandedPlayerSheet.initialPortraitPanelVisibility( + settingsManager: appEnvironment?.settingsManager + ) + ) } else { ProgressView() .controlSize(.large)