mirror of
https://github.com/yattee/yattee.git
synced 2026-10-28 07:27:18 +00:00
Add per-page element customization and panel settings polish
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.
This commit is contained in:
@@ -19,6 +19,14 @@ struct PanelPageEditorView: View {
|
||||
@State private var showDescription: Bool = true
|
||||
@State private var showStatsRow: Bool = true
|
||||
@State private var showChannelRow: Bool = true
|
||||
@State private var showAvatars: Bool = true
|
||||
@State private var showRelatedThumbnails: Bool = true
|
||||
@State private var showChapterThumbnails: Bool = true
|
||||
@State private var showHistory: Bool = true
|
||||
@State private var showQuality: Bool = true
|
||||
@State private var showAudio: Bool = true
|
||||
@State private var showSubtitles: Bool = true
|
||||
@State private var showRate: Bool = true
|
||||
|
||||
private var configuration: PanelPageConfiguration? {
|
||||
viewModel.panelSettings.configuration(for: pageType)
|
||||
@@ -37,8 +45,19 @@ struct PanelPageEditorView: View {
|
||||
regionSection
|
||||
}
|
||||
|
||||
if pageType == .info {
|
||||
switch pageType {
|
||||
case .info:
|
||||
infoElementsSection
|
||||
case .comments:
|
||||
commentsElementsSection
|
||||
case .related:
|
||||
relatedElementsSection
|
||||
case .chapters:
|
||||
chaptersElementsSection
|
||||
case .queue:
|
||||
queueElementsSection
|
||||
case .tracks:
|
||||
tracksElementsSection
|
||||
}
|
||||
}
|
||||
.navigationTitle(pageType.displayName)
|
||||
@@ -136,12 +155,151 @@ struct PanelPageEditorView: View {
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Comments Elements Section
|
||||
|
||||
private var commentsElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "panel.settings.comments.showAvatars"),
|
||||
isOn: $showAvatars
|
||||
)
|
||||
.onChange(of: showAvatars) { _, newValue in
|
||||
guard newValue != commentsSettings.effectiveShowAvatars else { return }
|
||||
viewModel.updatePanelCommentsSettingsSync { $0.showAvatars = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Related Elements Section
|
||||
|
||||
private var relatedElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "panel.settings.showThumbnails"),
|
||||
isOn: $showRelatedThumbnails
|
||||
)
|
||||
.onChange(of: showRelatedThumbnails) { _, newValue in
|
||||
guard newValue != relatedSettings.effectiveShowThumbnails else { return }
|
||||
viewModel.updatePanelRelatedSettingsSync { $0.showThumbnails = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Chapters Elements Section
|
||||
|
||||
private var chaptersElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "panel.settings.showThumbnails"),
|
||||
isOn: $showChapterThumbnails
|
||||
)
|
||||
.onChange(of: showChapterThumbnails) { _, newValue in
|
||||
guard newValue != chaptersSettings.effectiveShowThumbnails else { return }
|
||||
viewModel.updatePanelChaptersSettingsSync { $0.showThumbnails = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Queue Elements Section
|
||||
|
||||
private var queueElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "panel.settings.queue.showHistory"),
|
||||
isOn: $showHistory
|
||||
)
|
||||
.onChange(of: showHistory) { _, newValue in
|
||||
guard newValue != queueSettings.effectiveShowHistory else { return }
|
||||
viewModel.updatePanelQueueSettingsSync { $0.showHistory = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
} footer: {
|
||||
Text(String(localized: "panel.settings.queue.showHistory.footer"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Tracks Elements Section
|
||||
|
||||
private var tracksElementsSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "player.quality.video"),
|
||||
isOn: $showQuality
|
||||
)
|
||||
.onChange(of: showQuality) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowQuality else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showQuality = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "stream.audio"),
|
||||
isOn: $showAudio
|
||||
)
|
||||
.onChange(of: showAudio) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowAudio else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showAudio = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "stream.subtitles"),
|
||||
isOn: $showSubtitles
|
||||
)
|
||||
.onChange(of: showSubtitles) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowSubtitles else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showSubtitles = newValue }
|
||||
}
|
||||
|
||||
Toggle(
|
||||
String(localized: "controls.button.speed"),
|
||||
isOn: $showRate
|
||||
)
|
||||
.onChange(of: showRate) { _, newValue in
|
||||
guard newValue != tracksSettings.effectiveShowRate else { return }
|
||||
viewModel.updatePanelTracksSettingsSync { $0.showRate = newValue }
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "panel.settings.elements.header"))
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var infoSettings: InfoPageSettings {
|
||||
configuration?.infoSettings ?? InfoPageSettings()
|
||||
}
|
||||
|
||||
private var commentsSettings: CommentsPageSettings {
|
||||
configuration?.commentsSettings ?? CommentsPageSettings()
|
||||
}
|
||||
|
||||
private var relatedSettings: RelatedPageSettings {
|
||||
configuration?.relatedSettings ?? RelatedPageSettings()
|
||||
}
|
||||
|
||||
private var chaptersSettings: ChaptersPageSettings {
|
||||
configuration?.chaptersSettings ?? ChaptersPageSettings()
|
||||
}
|
||||
|
||||
private var queueSettings: QueuePageSettings {
|
||||
configuration?.queueSettings ?? QueuePageSettings()
|
||||
}
|
||||
|
||||
private var tracksSettings: TracksPageSettings {
|
||||
configuration?.tracksSettings ?? TracksPageSettings()
|
||||
}
|
||||
|
||||
private func syncLocalState() {
|
||||
isEnabled = configuration?.isEnabled ?? true
|
||||
region = configuration?.region ?? .primary
|
||||
@@ -149,6 +307,15 @@ struct PanelPageEditorView: View {
|
||||
showDescription = info.effectiveShowDescription
|
||||
showStatsRow = info.effectiveShowStatsRow
|
||||
showChannelRow = info.effectiveShowChannelRow
|
||||
showAvatars = commentsSettings.effectiveShowAvatars
|
||||
showRelatedThumbnails = relatedSettings.effectiveShowThumbnails
|
||||
showChapterThumbnails = chaptersSettings.effectiveShowThumbnails
|
||||
showHistory = queueSettings.effectiveShowHistory
|
||||
let tracks = tracksSettings
|
||||
showQuality = tracks.effectiveShowQuality
|
||||
showAudio = tracks.effectiveShowAudio
|
||||
showSubtitles = tracks.effectiveShowSubtitles
|
||||
showRate = tracks.effectiveShowRate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 showPageBarLabels: Bool = true
|
||||
|
||||
/// Corner layout exists on iPad and macOS only.
|
||||
static var supportsCornerLayout: Bool {
|
||||
@@ -31,6 +32,7 @@ struct PanelSettingsView: View {
|
||||
layoutSection
|
||||
}
|
||||
visibilitySection
|
||||
pageBarSection
|
||||
pagesSection
|
||||
}
|
||||
.navigationTitle(String(localized: "settings.playerControls.detailsPanel"))
|
||||
@@ -57,6 +59,11 @@ struct PanelSettingsView: View {
|
||||
|
||||
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
|
||||
@@ -97,6 +104,24 @@ struct PanelSettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -126,6 +151,14 @@ struct PanelSettingsView: View {
|
||||
} 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)
|
||||
@@ -145,6 +178,66 @@ struct PanelSettingsView: View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user