diff --git a/Yattee/Core/PlaybackCommands.swift b/Yattee/Core/PlaybackCommands.swift index 2630c6ac..5a54dd66 100644 --- a/Yattee/Core/PlaybackCommands.swift +++ b/Yattee/Core/PlaybackCommands.swift @@ -79,6 +79,13 @@ struct PlaybackCommands: Commands { // Display modes pipButton + + Divider() + + // Corner layout panels + toggleInfoPanelButton + toggleSidebarButton + toggleAllPanelsButton Divider() closeVideoButton @@ -309,6 +316,42 @@ struct PlaybackCommands: Commands { .disabled(!hasActiveVideo || !state.isPiPPossible || state.currentStream?.isAudioOnly == true) } + // MARK: - Corner Layout Panels + + private var isCornerLayoutActive: Bool { + isPlayerExpanded && navigationCoordinator.isCornerLayoutActive + } + + private var toggleInfoPanelButton: some View { + Button { + navigationCoordinator.cornerPanelToggleRequest = CornerPanelToggleRequest(target: .belowVideo) + } label: { + Text(String(localized: "menu.playback.toggleInfoPanel")) + } + .keyboardShortcut("i", modifiers: [.command, .option]) + .disabled(!isCornerLayoutActive) + } + + private var toggleSidebarButton: some View { + Button { + navigationCoordinator.cornerPanelToggleRequest = CornerPanelToggleRequest(target: .sideColumn) + } label: { + Text(String(localized: "menu.playback.toggleSidebar")) + } + .keyboardShortcut("s", modifiers: [.command, .option]) + .disabled(!isCornerLayoutActive) + } + + private var toggleAllPanelsButton: some View { + Button { + navigationCoordinator.cornerPanelToggleRequest = CornerPanelToggleRequest(target: .all) + } label: { + Text(String(localized: "menu.playback.toggleAllPanels")) + } + .keyboardShortcut("p", modifiers: [.command, .option]) + .disabled(!isCornerLayoutActive) + } + // MARK: - Close video button private var closeVideoButton: some View { diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 379bcf2a..359ab86a 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -6907,6 +6907,36 @@ } } }, + "menu.playback.toggleAllPanels" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toggle All Panels" + } + } + } + }, + "menu.playback.toggleInfoPanel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toggle Info Panel" + } + } + } + }, + "menu.playback.toggleSidebar" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toggle Sidebar" + } + } + } + }, "menu.playback.volumeDown" : { "localizations" : { "en" : { diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index a445efea..44c00334 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -165,6 +165,11 @@ final class NavigationCoordinator { /// not collapsed) — the seek pan must not begin from inside it. var cornerSideColumnEdgeRevealInset: CGFloat = 0 + /// Corner layout panel toggle requested from outside the layout (menu + /// bar keyboard shortcuts). Each request carries its own id so repeating + /// the same shortcut fires again. + var cornerPanelToggleRequest: CornerPanelToggleRequest? + /// Progress bar frame in screen coordinates (for gesture conflict resolution). var progressBarFrame: CGRect = .zero @@ -389,3 +394,18 @@ final class NavigationCoordinator { } } } + +/// Corner layout regions a panel toggle request targets. +enum CornerPanelToggle: Equatable { + /// The info panel below the video (with the comments pill on iOS). + case belowVideo + /// The tabbed side column on the trailing edge. + case sideColumn + /// Every region at once — same as the in-player panel toggle button. + case all +} + +struct CornerPanelToggleRequest: Equatable { + let target: CornerPanelToggle + let id = UUID() +} diff --git a/Yattee/Views/Player/CornerPlayerLayout.swift b/Yattee/Views/Player/CornerPlayerLayout.swift index 80e99a51..5ff9d3d9 100644 --- a/Yattee/Views/Player/CornerPlayerLayout.swift +++ b/Yattee/Views/Player/CornerPlayerLayout.swift @@ -652,6 +652,10 @@ struct CornerPlayerLayout: View { panelToggleGeneration += 1 } } + .onChange(of: appEnvironment?.navigationCoordinator.cornerPanelToggleRequest) { _, request in + guard let request else { return } + handlePanelToggleRequest(request.target) + } } // MARK: - Panels @@ -851,6 +855,50 @@ struct CornerPlayerLayout: View { } } + /// Keyboard shortcut toggles from the menu bar. `.all` is the toggle + /// button; the single-region toggles flip one region and keep the other + /// as it is on screen. + private func handlePanelToggleRequest(_ target: CornerPanelToggle) { + let showsBelow = isPanelVisible && isBelowPanelVisible && hasBelowVideoInfo + let showsSide = isPanelVisible && isSideColumnVisible && hasSideColumn + switch target { + case .all: + togglePanel() + case .belowVideo: + guard hasBelowVideoInfo else { return } + setRegionsVisible(below: !showsBelow, side: showsSide) + case .sideColumn: + guard hasSideColumn else { return } + setRegionsVisible(below: showsBelow, side: !showsSide) + } + } + + /// Shows exactly the given regions, animated like the toggle button. + /// Hiding the last visible region hides the whole panel instead (video + /// fullscreen state), leaving both regions armed for the next show. + private func setRegionsVisible(below: Bool, side: Bool) { + let showsAny = (below && hasBelowVideoInfo) || (side && hasSideColumn) + withAnimation(.easeInOut(duration: 0.25)) { + // Drop any in-flight drag geometry — the flags alone define + // the layout from here + isRevealDragActive = false + isSideColumnRevealDragActive = false + sideColumnDragOffset = 0 + belowPanelDragOffset = 0 + if !below || !showsAny { collapseBelowPanelUnderVideo() } + isBelowPanelVisible = showsAny ? below : true + isSideColumnVisible = showsAny ? side : true + isPanelVisible = showsAny + panelToggleGeneration += 1 + // Also gates the macOS window aspect lock + // (`isCornerPanelLayoutActive`) + if settingsManager?.landscapeDetailsPanelVisible != showsAny { + settingsManager?.landscapeDetailsPanelVisible = showsAny + settingsManager?.landscapeDetailsPanelLastVisible = showsAny + } + } + } + /// Hiding everything grows the video over the below panel, so an /// expanded panel (raised above the video) must drop back to its home /// geometry under the video first.