Add details panel preset settings UI and default visibility

Phase 2 of the player panels redesign:

- New "Details Panel" screen in Player Controls settings: page
  reorder/enable list with per-page editor (info element toggles),
  and a default panel visibility picker (hidden/visible/remember).
- PlayerControlsSettingsViewModel gains panelSettings bindings with
  the usual debounced-save flow; the last enabled page cannot be
  disabled.
- Built-in presets bumped to v9 with explicit default panelSettings.
- The preset's default visibility now drives the wide-layout panel
  on player open and on entering landscape, replacing the hard
  hidden resets; "remember" restores the new detailsPanelLastVisible
  setting recorded on user toggles. Applied once per expanded player
  session (flag on NavigationCoordinator survives re-mounts).
- WideScreenPlayerLayout picks up external visibility writes so the
  async-loaded default reaches the mounted layout.
- Unit tests for PanelSettings Codable compatibility (round-trip,
  old JSON, unknown page types/discriminators/enum values); removed
  stale PlayerInfoTab tests left from Phase 1.
This commit is contained in:
Arkadiusz Fal
2026-09-21 23:25:10 +02:00
parent d5285b2b83
commit c33d125553
15 changed files with 794 additions and 35 deletions

View File

@@ -11,7 +11,7 @@ extension LayoutPreset {
/// Bump this version whenever any built-in preset definition changes.
/// On launch, the app compares this against the last-applied version
/// and replaces stale built-in presets with fresh copies from code.
static let builtInPresetsVersion = 8
static let builtInPresetsVersion = 9
// MARK: - Built-in Preset IDs
@@ -131,7 +131,8 @@ extension LayoutPreset {
progressBarSettings: ProgressBarSettings(),
gesturesSettings: gesturesSettings,
playerPillSettings: playerPillSettings,
miniPlayerSettings: miniPlayerSettings
miniPlayerSettings: miniPlayerSettings,
panelSettings: .default
)
}
@@ -192,7 +193,8 @@ extension LayoutPreset {
progressBarSettings: ProgressBarSettings(),
gesturesSettings: nil,
playerPillSettings: nil,
miniPlayerSettings: MiniPlayerSettings()
miniPlayerSettings: MiniPlayerSettings(),
panelSettings: .default
)
}
@@ -285,7 +287,8 @@ extension LayoutPreset {
progressBarSettings: progressBarSettings,
gesturesSettings: gesturesSettings,
playerPillSettings: playerPillSettings,
miniPlayerSettings: miniPlayerSettings
miniPlayerSettings: miniPlayerSettings,
panelSettings: .default
)
}
@@ -336,7 +339,8 @@ extension LayoutPreset {
progressBarSettings: progressBarSettings,
gesturesSettings: nil,
playerPillSettings: nil,
miniPlayerSettings: MiniPlayerSettings()
miniPlayerSettings: MiniPlayerSettings(),
panelSettings: .default
)
}

View File

@@ -6,6 +6,7 @@
//
import Foundation
import SwiftUI
// MARK: - Panel Page Type
@@ -77,10 +78,22 @@ enum PanelLayoutMode: String, Codable, Sendable {
// MARK: - Default Visibility
/// Default panel visibility when the player opens on iPad/macOS.
enum PanelDefaultVisibility: String, Codable, Sendable {
enum PanelDefaultVisibility: String, Codable, CaseIterable, Sendable {
case hidden
case visible
case remember
/// Localized name for the settings picker.
var displayName: String {
switch self {
case .hidden:
return String(localized: "panel.defaultVisibility.hidden")
case .visible:
return String(localized: "panel.defaultVisibility.visible")
case .remember:
return String(localized: "panel.defaultVisibility.remember")
}
}
}
// MARK: - Per-Page Settings
@@ -288,6 +301,12 @@ struct PanelSettings: Codable, Hashable, Sendable {
self.showPageBarLabels = showPageBarLabels
}
/// In-memory cache of the active preset's panel settings, refreshed by the
/// player whenever the layout loads. Lets synchronous appear paths (e.g.
/// default-visibility resets) read the last known value before the async
/// layout fetch completes.
nonisolated(unsafe) static var cached: PanelSettings = .default
static let `default` = PanelSettings(pages: [
PanelPageConfiguration(pageType: .info, region: .belowVideo),
PanelPageConfiguration(pageType: .comments, region: .belowVideo),
@@ -328,6 +347,36 @@ struct PanelSettings: Codable, Hashable, Sendable {
enabledPages().contains { $0.pageType == pageType }
}
/// The stored configuration for a page type, if present.
func configuration(for pageType: PanelPageType) -> PanelPageConfiguration? {
normalizedPages.first { $0.pageType == pageType }
}
// MARK: - Mutations (settings UI)
/// Replaces the stored pages with the normalized list so mutations by
/// index operate on the same list the UI displays.
mutating func normalize() {
pages = normalizedPages
}
mutating func setEnabled(_ isEnabled: Bool, for pageType: PanelPageType) {
normalize()
guard let index = pages.firstIndex(where: { $0.pageType == pageType }) else { return }
pages[index].isEnabled = isEnabled
}
mutating func movePages(fromOffsets source: IndexSet, toOffset destination: Int) {
normalize()
pages.move(fromOffsets: source, toOffset: destination)
}
mutating func updateSettings(_ newSettings: PanelPageSettings?, for pageType: PanelPageType) {
normalize()
guard let index = pages.firstIndex(where: { $0.pageType == pageType }) else { return }
pages[index].settings = newSettings
}
private enum CodingKeys: String, CodingKey {
case pages
case layoutMode