mirror of
https://github.com/yattee/yattee.git
synced 2026-10-24 05:32:53 +00:00
Phase 5 of the player panels redesign: - Per-page element toggles now rendered and editable for every page: comments (author avatars), related (thumbnails), chapters (thumbnails), queue (previously played section), tracks (video/audio/subtitles/speed sections) - Page bar labels toggle (icons-only bar shows the selected page's name) - Layout mode schematic diagram in the details panel settings - NEW badge machinery for future page types (versionAdded on PanelPageType) - Typed per-page settings accessors on PanelPageConfiguration and VM update helpers for each page kind - Unit tests for per-page settings round-trip and accessor fallbacks The standalone quality selector sheet is unaffected: section gating defaults to everything visible and only the embedded tracks page passes preset settings through.
245 lines
7.6 KiB
Swift
245 lines
7.6 KiB
Swift
//
|
|
// PanelSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// Settings editor for the player details panel pages.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
#if os(iOS) || os(macOS)
|
|
|
|
struct PanelSettingsView: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state for immediate UI updates
|
|
@State private var defaultVisibility: PanelDefaultVisibility = .hidden
|
|
@State private var layoutMode: PanelLayoutMode = .stacked
|
|
@State private var showPageBarLabels: Bool = true
|
|
|
|
/// Corner layout exists on iPad and macOS only.
|
|
static var supportsCornerLayout: Bool {
|
|
#if os(macOS)
|
|
return true
|
|
#else
|
|
return UIDevice.current.userInterfaceIdiom == .pad
|
|
#endif
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
if Self.supportsCornerLayout {
|
|
layoutSection
|
|
}
|
|
visibilitySection
|
|
pageBarSection
|
|
pagesSection
|
|
}
|
|
.navigationTitle(String(localized: "settings.playerControls.detailsPanel"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#elseif os(macOS)
|
|
.formStyle(.grouped)
|
|
#endif
|
|
.opaqueSettingsFormBackground()
|
|
.toolbar {
|
|
#if os(iOS)
|
|
EditButton()
|
|
#endif
|
|
}
|
|
.onAppear {
|
|
syncLocalState()
|
|
}
|
|
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
|
syncLocalState()
|
|
}
|
|
}
|
|
|
|
// MARK: - Layout Section
|
|
|
|
private var layoutSection: some View {
|
|
Section {
|
|
PanelLayoutSchematic(mode: layoutMode)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 4)
|
|
.listRowBackground(Color.clear)
|
|
|
|
Picker(
|
|
String(localized: "panel.settings.layoutMode"),
|
|
selection: $layoutMode
|
|
) {
|
|
ForEach(PanelLayoutMode.allCases, id: \.self) { mode in
|
|
Text(mode.displayName).tag(mode)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: layoutMode) { _, newValue in
|
|
guard newValue != viewModel.panelLayoutMode else { return }
|
|
viewModel.syncPanelLayoutMode(newValue)
|
|
}
|
|
} footer: {
|
|
Text(String(localized: "panel.settings.layoutMode.footer"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Visibility Section
|
|
|
|
private var visibilitySection: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "panel.settings.defaultVisibility"),
|
|
selection: $defaultVisibility
|
|
) {
|
|
ForEach(PanelDefaultVisibility.allCases, id: \.self) { visibility in
|
|
Text(visibility.displayName).tag(visibility)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: defaultVisibility) { _, newValue in
|
|
guard newValue != viewModel.panelDefaultVisibility else { return }
|
|
viewModel.syncPanelDefaultVisibility(newValue)
|
|
}
|
|
} footer: {
|
|
Text(String(localized: "panel.settings.defaultVisibility.footer"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Page Bar Section
|
|
|
|
private var pageBarSection: some View {
|
|
Section {
|
|
Toggle(
|
|
String(localized: "panel.settings.pageBarLabels"),
|
|
isOn: $showPageBarLabels
|
|
)
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: showPageBarLabels) { _, newValue in
|
|
guard newValue != viewModel.panelShowPageBarLabels else { return }
|
|
viewModel.syncPanelShowPageBarLabels(newValue)
|
|
}
|
|
} footer: {
|
|
Text(String(localized: "panel.settings.pageBarLabels.footer"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Pages Section
|
|
|
|
private var pagesSection: some View {
|
|
Section {
|
|
ForEach(viewModel.panelPages) { page in
|
|
pageRow(for: page)
|
|
}
|
|
.onMove { source, destination in
|
|
viewModel.movePanelPages(fromOffsets: source, toOffset: destination)
|
|
}
|
|
} header: {
|
|
Text(String(localized: "panel.settings.pages.header"))
|
|
} footer: {
|
|
if viewModel.canEditActivePreset {
|
|
Text(String(localized: "panel.settings.pages.footer"))
|
|
} else {
|
|
Text(String(localized: "settings.playerControls.duplicateToEdit"))
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func pageRow(for page: PanelPageConfiguration) -> some View {
|
|
NavigationLink {
|
|
PanelPageEditorView(pageType: page.pageType, viewModel: viewModel)
|
|
} label: {
|
|
HStack {
|
|
Label(page.pageType.displayName, systemImage: page.pageType.systemImage)
|
|
if page.pageType.isRecentlyAdded {
|
|
Text(String(localized: "common.badge.new"))
|
|
.font(.caption2.weight(.bold))
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 5)
|
|
.padding(.vertical, 2)
|
|
.background(Capsule().fill(.tint))
|
|
}
|
|
Spacer()
|
|
if page.isEnabled, layoutMode == .corner, Self.supportsCornerLayout {
|
|
Text(page.region.displayName)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(page.isEnabled
|
|
? String(localized: "common.on")
|
|
: String(localized: "common.off"))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func syncLocalState() {
|
|
defaultVisibility = viewModel.panelDefaultVisibility
|
|
layoutMode = viewModel.panelLayoutMode
|
|
showPageBarLabels = viewModel.panelShowPageBarLabels
|
|
}
|
|
}
|
|
|
|
// MARK: - Layout Schematic
|
|
|
|
/// Small diagram of the player window for the layout mode picker: the video
|
|
/// area plus where the details panel regions sit in each mode.
|
|
private struct PanelLayoutSchematic: View {
|
|
let mode: PanelLayoutMode
|
|
|
|
private let cornerRadius: CGFloat = 4
|
|
|
|
var body: some View {
|
|
HStack(spacing: 3) {
|
|
if mode == .corner {
|
|
VStack(spacing: 3) {
|
|
videoBlock
|
|
panelBlock
|
|
.frame(height: 26)
|
|
}
|
|
} else {
|
|
videoBlock
|
|
}
|
|
|
|
panelBlock
|
|
.frame(width: 44)
|
|
}
|
|
.padding(6)
|
|
.frame(width: 180, height: 110)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(.quaternary.opacity(0.4))
|
|
}
|
|
.animation(.easeInOut(duration: 0.25), value: mode)
|
|
}
|
|
|
|
private var videoBlock: some View {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.fill(.secondary)
|
|
.overlay {
|
|
Image(systemName: "play.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.background)
|
|
}
|
|
}
|
|
|
|
private var panelBlock: some View {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.fill(.quaternary)
|
|
.overlay {
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
ForEach(0..<3, id: \.self) { _ in
|
|
Capsule()
|
|
.fill(.secondary.opacity(0.5))
|
|
.frame(height: 3)
|
|
}
|
|
}
|
|
.padding(6)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|