Rework corner layout with portrait-style info panel below the video

The corner layout now always renders the info page as a portrait-style
panel below the video (collapsible description, drag to hide/expand),
while the side column holds tabs for the remaining pages. Key pieces:

- Below-video panel reuses PortraitDetailsPanel (no comments pill,
  square corners, chapters/related stay in the side column tabs)
- Drag down hides the panel with the video recentering in lockstep;
  swipe up on the video reveals it following the finger
- Fullscreen button toggles the panels in corner layout (controls hide
  first so their resize never plays visibly); status bar and home
  indicator hide only while the panels are hidden
- Panel visibility and column width are seeded at init from a warmed
  PlayerControlsLayout cache, so the first frame resolves corner mode
  without flashing the default layout or animating the panels in
- Dismiss pan is blocked inside the below-video panel frame
  (cornerBelowPanelFrame in NavigationCoordinator)
- Resize grabber slimmed to match the panel drag handle look
This commit is contained in:
Arkadiusz Fal
2026-09-23 07:25:02 +02:00
parent ec13fda115
commit 05a03723f6
14 changed files with 586 additions and 435 deletions

View File

@@ -43,6 +43,8 @@ struct PlayerControlsView: View {
var isPanelVisible: Bool = true
var panelSide: FloatingPanelSide = .right
var isPanelPinned: Bool = false
/// Corner layout: fullscreen button toggles the panels
var isCornerLayout: Bool = false
/// Safe area padding from layout (when panel is pinned)
var layoutLeadingSafeArea: CGFloat = 0
var layoutTrailingSafeArea: CGFloat = 0
@@ -230,6 +232,14 @@ struct PlayerControlsView: View {
cancelHideTimer()
}
}
.onChange(of: isPanelVisible) { oldValue, newValue in
// Corner layout: any panel toggle resizes the controls area —
// keep the controls hidden while that transition plays
if isCornerLayout, oldValue != newValue {
showControls = false
cancelHideTimer()
}
}
.onChange(of: playerState.retryState.exhausted) { _, exhausted in
// Hide controls when retries are exhausted (retry exhausted overlay takes over)
if exhausted {
@@ -389,6 +399,7 @@ struct PlayerControlsView: View {
isOrientationLocked: isOrientationLocked,
isPanelVisible: isPanelVisible,
isPanelPinned: isPanelPinned,
isCornerLayout: isCornerLayout,
panelSide: panelSide,
isIPad: isIPad,
showVolumeControls: showVolumeControls,
@@ -415,6 +426,10 @@ struct PlayerControlsView: View {
onTogglePiP: onTogglePiP,
onToggleFullscreen: onToggleFullscreen,
onToggleDetailsVisibility: onToggleDetailsVisibility,
onHideControls: { [self] in
showControls = false
cancelHideTimer()
},
onToggleOrientationLock: onToggleOrientationLock,
onTogglePanel: onTogglePanel,
onTogglePanscan: onTogglePanscan,
@@ -484,7 +499,11 @@ struct PlayerControlsView: View {
// bottomExtraPadding handles positioning, so no safe area needed
// In landscape (widescreen), cap the safe area since home indicator needs less clearance
let floatingPanelActive = isPanelVisible && !isPanelPinned
let needsBottomSafeArea = (isWideScreenLayout || isFullscreen) && !floatingPanelActive
// Corner layout with panels visible: the controls frame ends at the
// video area mid-screen, so the window's bottom safe area (incl. the
// PiP inset) must not push the bottom bar up from the video edge
let cornerPanelsActive = isCornerLayout && isPanelVisible
let needsBottomSafeArea = (isWideScreenLayout || isFullscreen) && !floatingPanelActive && !cornerPanelsActive
let bottomSafeArea = isWideScreenLayout ? min(safeArea.bottom, 12) : safeArea.bottom
let bottomPadding: CGFloat = 2 + (needsBottomSafeArea ? bottomSafeArea : 0)