diff --git a/Yattee/Core/PlaybackCommands.swift b/Yattee/Core/PlaybackCommands.swift index a65e6607..2630c6ac 100644 --- a/Yattee/Core/PlaybackCommands.swift +++ b/Yattee/Core/PlaybackCommands.swift @@ -328,9 +328,8 @@ struct PlaybackCommands: Commands { // Clear the queue when closing video appEnvironment.queueManager.clearQueue() - // Reset panel state when closing player + // Reset panel visibility when closing player settingsManager.landscapeDetailsPanelVisible = false - settingsManager.landscapeDetailsPanelPinned = false // Stop player FIRST before dismissing window playerService.stop() diff --git a/Yattee/Core/Settings/SettingsKey.swift b/Yattee/Core/Settings/SettingsKey.swift index 5cf9cda5..ad33a8ee 100644 --- a/Yattee/Core/Settings/SettingsKey.swift +++ b/Yattee/Core/Settings/SettingsKey.swift @@ -128,7 +128,6 @@ enum SettingsKey: String, CaseIterable { case tvVideoSyncMode // Details panel - case floatingDetailsPanelSide // Landscape only - which side the panel appears on case floatingDetailsPanelWidth // Resizable panel width in wide layout case landscapeDetailsPanelVisible case landscapeDetailsPanelPinned @@ -167,7 +166,7 @@ enum SettingsKey: String, CaseIterable { .sidebarChannelsEnabled, .sidebarMaxChannels, .sidebarChannelSort, .sidebarChannelsLimitEnabled, .sidebarPlaylistsEnabled, .sidebarMaxPlaylists, .sidebarPlaylistSort, .sidebarPlaylistsLimitEnabled, // Player details panel — iOS/iPadOS only, different on other platforms - .floatingDetailsPanelSide, .floatingDetailsPanelWidth, + .floatingDetailsPanelWidth, .landscapeDetailsPanelVisible, .landscapeDetailsPanelPinned, .detailsPanelLastVisible, .cornerPanelColumnWidth, // Video swipe actions — touch-gesture feature diff --git a/Yattee/Core/Settings/SettingsManager+Advanced.swift b/Yattee/Core/Settings/SettingsManager+Advanced.swift index 8a306872..5a9d058a 100644 --- a/Yattee/Core/Settings/SettingsManager+Advanced.swift +++ b/Yattee/Core/Settings/SettingsManager+Advanced.swift @@ -234,19 +234,6 @@ extension SettingsManager { // MARK: - Details Panel Settings - /// Which side the floating details panel appears on in landscape layout. - /// Default is right side. - var floatingDetailsPanelSide: FloatingPanelSide { - get { - if let cached = _floatingDetailsPanelSide { return cached } - return FloatingPanelSide(rawValue: string(for: .floatingDetailsPanelSide) ?? "") ?? .left - } - set { - _floatingDetailsPanelSide = newValue - set(newValue.rawValue, for: .floatingDetailsPanelSide) - } - } - /// Default panel width in wide layout. static let defaultFloatingDetailsPanelWidth: CGFloat = 400 @@ -307,12 +294,14 @@ extension SettingsManager { } } - /// Whether the details panel is pinned in landscape layout. - /// Default is false (floating mode). + /// Whether the details panel is pinned (side-by-side) in the wide layout. + /// iPad/macOS default to pinned; iPhone is floating-only and always + /// resolves to `false` regardless of the stored value. var landscapeDetailsPanelPinned: Bool { get { + if PanelSettings.isSectionsOnlyDevice { return false } if let cached = _landscapeDetailsPanelPinned { return cached } - return bool(for: .landscapeDetailsPanelPinned, default: false) + return bool(for: .landscapeDetailsPanelPinned, default: true) } set { _landscapeDetailsPanelPinned = newValue diff --git a/Yattee/Core/Settings/SettingsTypes.swift b/Yattee/Core/Settings/SettingsTypes.swift index 46b60482..d405417a 100644 --- a/Yattee/Core/Settings/SettingsTypes.swift +++ b/Yattee/Core/Settings/SettingsTypes.swift @@ -322,16 +322,16 @@ enum SponsorBlockCategory: String, CaseIterable, Codable, Sendable { // MARK: - Floating Panel -/// Which side the floating details panel appears on in widescreen layout. +/// Which side the details panel appears on in the standard wide layout. enum FloatingPanelSide: String, CaseIterable, Codable { case left case right - /// The opposite side. - var opposite: FloatingPanelSide { + /// Localized name for the settings picker. + var displayName: String { switch self { - case .left: return .right - case .right: return .left + case .left: return String(localized: "panel.side.left") + case .right: return String(localized: "panel.side.right") } } diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index d4380e25..6b190e80 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -178,7 +178,6 @@ final class SettingsManager { var _tvVideoSyncMode: TVVideoSyncMode? // Details panel settings - var _floatingDetailsPanelSide: FloatingPanelSide? var _floatingDetailsPanelWidth: CGFloat? var _landscapeDetailsPanelVisible: Bool? var _landscapeDetailsPanelPinned: Bool? @@ -530,7 +529,6 @@ final class SettingsManager { _tvMatchDisplayDynamicRange = nil _tvAudioDelayMs = nil _tvVideoSyncMode = nil - _floatingDetailsPanelSide = nil _floatingDetailsPanelWidth = nil _landscapeDetailsPanelVisible = nil _landscapeDetailsPanelPinned = nil diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 25aeecfa..dce55d03 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -7560,6 +7560,39 @@ } } }, + "panel.settings.side" : { + "comment" : "Details panel side picker (iPad/macOS standard layout)", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panel Side" + } + } + } + }, + "panel.side.left" : { + "comment" : "Details panel side option", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Left" + } + } + } + }, + "panel.side.right" : { + "comment" : "Details panel side option", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Right" + } + } + } + }, "peertube.explore.added" : { "localizations" : { "en" : { @@ -19994,26 +20027,6 @@ } } }, - "wideLayoutPanel.alignmentButton.moveLeft" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move Panel Left" - } - } - } - }, - "wideLayoutPanel.alignmentButton.moveRight" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move Panel Right" - } - } - } - }, "wideLayoutPanel.pinButton.pin" : { "localizations" : { "en" : { diff --git a/Yattee/Models/PlayerControls/PanelSettings.swift b/Yattee/Models/PlayerControls/PanelSettings.swift index f6da9bfb..808565b1 100644 --- a/Yattee/Models/PlayerControls/PanelSettings.swift +++ b/Yattee/Models/PlayerControls/PanelSettings.swift @@ -359,16 +359,22 @@ struct PanelSettings: Codable, Hashable, Sendable { /// Whether the page bar shows labels next to icons. var showPageBarLabels: Bool? + /// Which side the details panel sits on in the standard wide layout on + /// iPad/macOS. iPhone ignores it (always right). Effective default: `.right`. + var wideLayoutSide: FloatingPanelSide? + init( pages: [PanelPageConfiguration], layoutMode: PanelLayoutMode? = nil, defaultVisibility: PanelDefaultVisibility? = nil, - showPageBarLabels: Bool? = nil + showPageBarLabels: Bool? = nil, + wideLayoutSide: FloatingPanelSide? = nil ) { self.pages = pages self.layoutMode = layoutMode self.defaultVisibility = defaultVisibility self.showPageBarLabels = showPageBarLabels + self.wideLayoutSide = wideLayoutSide } /// In-memory cache of the active preset's panel settings, refreshed by the @@ -388,6 +394,7 @@ struct PanelSettings: Codable, Hashable, Sendable { var effectiveLayoutMode: PanelLayoutMode { layoutMode ?? .stacked } var effectiveDefaultVisibility: PanelDefaultVisibility { defaultVisibility ?? .hidden } var effectiveShowPageBarLabels: Bool { showPageBarLabels ?? true } + var effectiveWideLayoutSide: FloatingPanelSide { wideLayoutSide ?? .right } /// Pages the panel should offer, in order. Pass a region to filter for /// corner mode; `nil` returns enabled pages from all regions. @@ -488,6 +495,7 @@ struct PanelSettings: Codable, Hashable, Sendable { case layoutMode case defaultVisibility case showPageBarLabels + case wideLayoutSide } init(from decoder: Decoder) throws { @@ -509,6 +517,7 @@ struct PanelSettings: Codable, Hashable, Sendable { layoutMode = try? container.decodeIfPresent(PanelLayoutMode.self, forKey: .layoutMode) defaultVisibility = try? container.decodeIfPresent(PanelDefaultVisibility.self, forKey: .defaultVisibility) showPageBarLabels = try container.decodeIfPresent(Bool.self, forKey: .showPageBarLabels) + wideLayoutSide = try? container.decodeIfPresent(FloatingPanelSide.self, forKey: .wideLayoutSide) } func encode(to encoder: Encoder) throws { @@ -517,6 +526,7 @@ struct PanelSettings: Codable, Hashable, Sendable { try container.encodeIfPresent(layoutMode, forKey: .layoutMode) try container.encodeIfPresent(defaultVisibility, forKey: .defaultVisibility) try container.encodeIfPresent(showPageBarLabels, forKey: .showPageBarLabels) + try container.encodeIfPresent(wideLayoutSide, forKey: .wideLayoutSide) } } diff --git a/Yattee/Models/PlayerControls/PlayerControlsLayout.swift b/Yattee/Models/PlayerControls/PlayerControlsLayout.swift index 05ebdff5..ee479584 100644 --- a/Yattee/Models/PlayerControls/PlayerControlsLayout.swift +++ b/Yattee/Models/PlayerControls/PlayerControlsLayout.swift @@ -33,11 +33,6 @@ struct PlayerControlsLayout: Codable, Hashable, Sendable { /// Mini player settings. Optional for backward compatibility. var miniPlayerSettings: MiniPlayerSettings? - /// Unused: the player reads the panel side from SettingsManager - /// (`floatingDetailsPanelSide`). Kept only so old presets that saved this - /// field still decode. - var wideLayoutPanelAlignment: FloatingPanelSide? - /// Details panel pages configuration. Optional for backward compatibility. var panelSettings: PanelSettings? diff --git a/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift b/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift index 5662b1af..c2d61ab6 100644 --- a/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift +++ b/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift @@ -818,6 +818,18 @@ final class PlayerControlsSettingsViewModel { } } + /// Current panel side in the standard wide layout (iPad/macOS). + var panelWideLayoutSide: FloatingPanelSide { + panelSettings.effectiveWideLayoutSide + } + + /// Synchronously updates the panel side in the standard wide layout. + func syncPanelWideLayoutSide(_ side: FloatingPanelSide) { + updatePanelSettingsSync { settings in + settings.wideLayoutSide = side + } + } + /// Synchronously updates a page's corner-layout region assignment. func setPanelPageRegion(_ region: PanelRegion, for pageType: PanelPageType) { updatePanelSettingsSync { settings in diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift b/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift index fe25a3ff..86ad4f8d 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift @@ -92,9 +92,8 @@ extension ExpandedPlayerSheet { // Clear the queue when closing video appEnvironment?.queueManager.clearQueue() - // Reset panel state when closing player + // Reset panel visibility when closing player appEnvironment?.settingsManager.landscapeDetailsPanelVisible = false - appEnvironment?.settingsManager.landscapeDetailsPanelPinned = false // Stop player FIRST before dismissing window // This ensures MPVRenderView and backend are properly cleaned up diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 8243b460..263db2c0 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -999,7 +999,6 @@ private func applyPanelDefaultVisibility(_ settings: PanelSettings, settingsMana switch settings.effectiveDefaultVisibility { case .hidden: settingsManager.landscapeDetailsPanelVisible = false - settingsManager.landscapeDetailsPanelPinned = false return false case .visible: settingsManager.landscapeDetailsPanelVisible = true diff --git a/Yattee/Views/Player/FloatingDetailsPanel.swift b/Yattee/Views/Player/FloatingDetailsPanel.swift index 9a2bd24d..e2a780eb 100644 --- a/Yattee/Views/Player/FloatingDetailsPanel.swift +++ b/Yattee/Views/Player/FloatingDetailsPanel.swift @@ -23,7 +23,6 @@ struct FloatingDetailsPanel: View { @Environment(\.colorScheme) private var systemColorScheme let onPinToggle: () -> Void - let onAlignmentToggle: () -> Void let isPinned: Bool let panelSide: FloatingPanelSide let onChannelTap: (() -> Void)? @@ -95,6 +94,11 @@ struct FloatingDetailsPanel: View { PanelSettings.isSectionsOnlyDevice } + /// iPhone is floating-only: no pin button on the grabber. + private var showsPinButton: Bool { + !PanelSettings.isSectionsOnlyDevice + } + /// Whether the comments pill should route to the comments page instead of /// the overlay. private var isCommentsPageAvailable: Bool { @@ -189,23 +193,16 @@ struct FloatingDetailsPanel: View { isDragActive: $isDragHandleActive ) .overlay(alignment: .center) { - PanelPinButton( - isPinned: isPinned, - panelSide: panelSide, - onPinToggle: onPinToggle, - isDragHandleActive: $isDragHandleActive - ) - .offset(y: -65) - .zIndex(10) // Ensure pin button appears above panel content - } - .overlay(alignment: .center) { - PanelAlignmentButton( - panelSide: panelSide, - onAlignmentToggle: onAlignmentToggle, - isDragHandleActive: $isDragHandleActive - ) - .offset(y: 65) - .zIndex(10) + if showsPinButton { + PanelPinButton( + isPinned: isPinned, + panelSide: panelSide, + onPinToggle: onPinToggle, + isDragHandleActive: $isDragHandleActive + ) + .offset(y: -65) + .zIndex(10) // Ensure pin button appears above panel content + } } .zIndex(10) // Ensure grabber overlay appears above panel } diff --git a/Yattee/Views/Player/PanelAlignmentButton.swift b/Yattee/Views/Player/PanelAlignmentButton.swift deleted file mode 100644 index 53fe2240..00000000 --- a/Yattee/Views/Player/PanelAlignmentButton.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// PanelAlignmentButton.swift -// Yattee -// -// Floating alignment toggle button for the details panel in wide layout. -// - -import SwiftUI - -#if os(iOS) || os(macOS) - -/// A floating button that toggles the panel between left and right sides. -/// Mirrors the style of PanelPinButton: 36pt glass circle, same shadow and haptic. -struct PanelAlignmentButton: View { - @Environment(\.appEnvironment) private var appEnvironment - - let panelSide: FloatingPanelSide - let onAlignmentToggle: () -> Void - - /// Whether the drag handle is currently active (being dragged or hovered). - @Binding var isDragHandleActive: Bool - - /// Button diameter - private static let buttonSize: CGFloat = 36 - - var body: some View { - Button { - triggerHaptic() - onAlignmentToggle() - } label: { - Image(systemName: panelSide == .left ? "arrow.right" : "arrow.left") - .font(.system(size: 16, weight: .medium)) - .foregroundStyle(.primary) - .frame(width: Self.buttonSize, height: Self.buttonSize) - .glassBackground(.regular, in: .circle, fallback: .thinMaterial) - .shadow(color: .black.opacity(0.15), radius: 8, y: 2) - .contentShape(Circle()) - } - .buttonStyle(.plain) - .accessibilityLabel(panelSide == .left - ? String(localized: "wideLayoutPanel.alignmentButton.moveRight") - : String(localized: "wideLayoutPanel.alignmentButton.moveLeft")) - } - - // MARK: - Haptic Feedback - - private func triggerHaptic() { - #if os(iOS) - appEnvironment?.settingsManager.triggerHapticFeedback(for: .subscribeButton) - #endif - } -} - -#endif diff --git a/Yattee/Views/Player/WideScreenPlayerLayout.swift b/Yattee/Views/Player/WideScreenPlayerLayout.swift index 8c699389..7e0c2168 100644 --- a/Yattee/Views/Player/WideScreenPlayerLayout.swift +++ b/Yattee/Views/Player/WideScreenPlayerLayout.swift @@ -7,6 +7,10 @@ // - Overlay: Panel floats over full-width player, appears with controls // - Pinned: Player resizes to make room for panel // +// iPhone uses the overlay mode only, always on the right. iPad/macOS default +// to pinned, can unpin with the pin button, and pick the side in the preset's +// panel settings. +// import SwiftUI @@ -44,9 +48,11 @@ struct WideScreenPlayerLayout: View { private var video: Video? { playerState?.currentVideo } private var panelSide: FloatingPanelSide { - settingsManager?.floatingDetailsPanelSide ?? .left + if PanelSettings.isSectionsOnlyDevice { return .right } + return playerControlsLayout.effectivePanelSettings.effectiveWideLayoutSide } + /// `landscapeDetailsPanelPinned` already resolves to `false` on iPhone. private var isPanelPinned: Bool { settingsManager?.landscapeDetailsPanelPinned ?? false } @@ -362,11 +368,6 @@ struct WideScreenPlayerLayout: View { } } }, - onAlignmentToggle: { - withAnimation(.easeInOut(duration: 0.3)) { - settingsManager?.floatingDetailsPanelSide = panelSide.opposite - } - }, isPinned: isPanelPinned, panelSide: panelSide, onChannelTap: onChannelTap, diff --git a/Yattee/Views/Settings/PlayerControls/PanelSettingsView.swift b/Yattee/Views/Settings/PlayerControls/PanelSettingsView.swift index 6eee3402..0649411c 100644 --- a/Yattee/Views/Settings/PlayerControls/PanelSettingsView.swift +++ b/Yattee/Views/Settings/PlayerControls/PanelSettingsView.swift @@ -15,6 +15,7 @@ struct PanelSettingsView: View { // Local state for immediate UI updates @State private var defaultVisibility: PanelDefaultVisibility = .hidden @State private var layoutMode: PanelLayoutMode = .stacked + @State private var wideLayoutSide: FloatingPanelSide = .right @State private var showPageBarLabels: Bool = true /// Corner layout exists on iPad and macOS only. @@ -64,7 +65,7 @@ struct PanelSettingsView: View { private var layoutSection: some View { Section { - PanelLayoutSchematic(mode: layoutMode) + PanelLayoutSchematic(mode: layoutMode, side: wideLayoutSide) .frame(maxWidth: .infinity) .padding(.vertical, 4) .listRowBackground(Color.clear) @@ -82,6 +83,22 @@ struct PanelSettingsView: View { guard newValue != viewModel.panelLayoutMode else { return } viewModel.syncPanelLayoutMode(newValue) } + + if layoutMode == .stacked { + Picker( + String(localized: "panel.settings.side"), + selection: $wideLayoutSide + ) { + ForEach(FloatingPanelSide.allCases, id: \.self) { side in + Text(side.displayName).tag(side) + } + } + .disabled(!viewModel.canEditActivePreset) + .onChange(of: wideLayoutSide) { _, newValue in + guard newValue != viewModel.panelWideLayoutSide else { return } + viewModel.syncPanelWideLayoutSide(newValue) + } + } } footer: { Text(String(localized: "panel.settings.layoutMode.footer")) } @@ -187,6 +204,7 @@ struct PanelSettingsView: View { private func syncLocalState() { defaultVisibility = viewModel.panelDefaultVisibility layoutMode = viewModel.panelLayoutMode + wideLayoutSide = viewModel.panelWideLayoutSide showPageBarLabels = viewModel.panelShowPageBarLabels } } @@ -197,11 +215,18 @@ struct PanelSettingsView: View { /// area plus where the details panel regions sit in each mode. private struct PanelLayoutSchematic: View { let mode: PanelLayoutMode + /// Panel side in the standard layout; the corner column is always right. + let side: FloatingPanelSide private let cornerRadius: CGFloat = 4 var body: some View { HStack(spacing: 3) { + if mode == .stacked && side == .left { + panelBlock + .frame(width: 44) + } + if mode == .corner { VStack(spacing: 3) { videoBlock @@ -212,8 +237,10 @@ private struct PanelLayoutSchematic: View { videoBlock } - panelBlock - .frame(width: 44) + if mode == .corner || side == .right { + panelBlock + .frame(width: 44) + } } .padding(6) .frame(width: 180, height: 110) @@ -222,6 +249,7 @@ private struct PanelLayoutSchematic: View { .fill(.quaternary.opacity(0.4)) } .animation(.easeInOut(duration: 0.25), value: mode) + .animation(.easeInOut(duration: 0.25), value: side) } private var videoBlock: some View {