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.
This commit is contained in:
Arkadiusz Fal
2026-09-22 23:53:32 +02:00
parent 1ff3e62852
commit 3e5b387532
5 changed files with 85 additions and 9 deletions

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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)