Simplify the wide-layout details panel: floating-only on iPhone, side as a preset setting

iPhone landscape now uses the floating panel only, always on the right;
the pin and side-toggle buttons on the grabber are gone and the pinned
setting resolves to false on phone.

iPad and macOS default to the pinned side-by-side layout, keep the pin
button to switch to floating, and pick the panel side in the preset's
panel settings instead of an on-screen arrow. The pinned state persists
as a layout preference: closing the player or the hidden default
visibility no longer resets it.

Removes PanelAlignmentButton, the global floatingDetailsPanelSide setting
and its key, FloatingPanelSide.opposite, and the legacy
wideLayoutPanelAlignment preset field.
This commit is contained in:
Arkadiusz Fal
2026-09-26 22:48:11 +02:00
parent c33d66ca1f
commit 4210b0ab05
15 changed files with 122 additions and 137 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" : {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<PlayerContent: View>: 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<PlayerContent: View>: View {
}
}
},
onAlignmentToggle: {
withAnimation(.easeInOut(duration: 0.3)) {
settingsManager?.floatingDetailsPanelSide = panelSide.opposite
}
},
isPinned: isPanelPinned,
panelSide: panelSide,
onChannelTap: onChannelTap,

View File

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