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

@@ -790,6 +790,84 @@ final class PlayerControlsSettingsViewModel {
queueDebouncedSave(preset: preset)
}
// MARK: - Details Panel Settings
/// Current details panel settings from active preset, or default.
var panelSettings: PanelSettings {
activePreset?.layout.effectivePanelSettings ?? .default
}
/// Ordered pages list for the settings UI (includes disabled pages).
var panelPages: [PanelPageConfiguration] {
panelSettings.normalizedPages
}
/// Number of enabled panel pages.
var enabledPanelPageCount: Int {
panelSettings.enabledPages().count
}
/// Current default panel visibility.
var panelDefaultVisibility: PanelDefaultVisibility {
panelSettings.effectiveDefaultVisibility
}
/// Synchronously updates the default panel visibility.
func syncPanelDefaultVisibility(_ visibility: PanelDefaultVisibility) {
updatePanelSettingsSync { settings in
settings.defaultVisibility = visibility
}
}
/// Enables or disables a panel page. The last enabled page cannot be
/// disabled — the panel always needs at least one page.
func setPanelPageEnabled(_ pageType: PanelPageType, isEnabled: Bool) {
if !isEnabled {
let enabled = panelSettings.enabledPages()
if enabled.count <= 1, enabled.first?.pageType == pageType {
return
}
}
updatePanelSettingsSync { settings in
settings.setEnabled(isEnabled, for: pageType)
}
}
/// Moves pages within the panel page order.
func movePanelPages(fromOffsets source: IndexSet, toOffset destination: Int) {
updatePanelSettingsSync { settings in
settings.movePages(fromOffsets: source, toOffset: destination)
}
}
/// Synchronously updates the info page element toggles.
func updatePanelInfoSettingsSync(_ mutation: (inout InfoPageSettings) -> Void) {
updatePanelSettingsSync { settings in
var info = settings.configuration(for: .info)?.infoSettings ?? InfoPageSettings()
mutation(&info)
settings.updateSettings(.info(info), for: .info)
}
}
/// Synchronously updates details panel settings with immediate UI feedback.
/// Saves are debounced to avoid rapid disk writes.
/// - Parameter mutation: A closure that mutates the panel settings.
func updatePanelSettingsSync(_ mutation: (inout PanelSettings) -> Void) {
guard var preset = activePreset, !preset.isBuiltIn else {
error = "Cannot edit built-in presets. Duplicate it first."
return
}
var layout = preset.layout
var settings = layout.effectivePanelSettings
mutation(&settings)
layout.panelSettings = settings
preset = preset.withUpdatedLayout(layout)
self.activePreset = preset
queueDebouncedSave(preset: preset)
}
// MARK: - System Controls & Volume
/// Current system controls mode from active preset, or default.