Files
yattee/Yattee/Views/Settings/SettingsView.swift
Arkadiusz Fal 05bbd449f8 Drive macOS player control bars from presets
Render the macOS control bar button row and top bar from the active
preset's sections via a new MacOSControlsSectionRenderer, replacing the
hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop
button type and macOS-specific button availability lists, and adapt the
player controls settings editors (preset editor, section editor, button
configuration) for macOS.

Also apply the preset font style to the control bar time labels, and
update the built-in macOS Default preset order (queue before transport,
fullscreen at the end), bumping builtInPresetsVersion to 7.
2026-05-31 16:55:39 +02:00

409 lines
16 KiB
Swift

//
// SettingsView.swift
// Yattee
//
// Main settings view.
//
import SwiftUI
struct SettingsView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.appEnvironment) private var appEnvironment
var showCloseButton: Bool = true
#if os(macOS)
@State private var selectedSection: SettingsSection? = .sources
#endif
var body: some View {
#if os(macOS)
macOSSettings
.frame(minWidth: 600, minHeight: 520)
#elseif os(tvOS)
tvOSSettings
#else
iOSSettings
#endif
}
// MARK: - macOS Settings
#if os(macOS)
private var macOSSettings: some View {
NavigationSplitView {
VStack(spacing: 0) {
List(SettingsSection.allCases, selection: $selectedSection) { section in
Label(section.title, systemImage: section.icon)
.tag(section)
}
.listStyle(.sidebar)
Divider()
macOSSidebarFooter
}
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
} detail: {
if appEnvironment != nil {
NavigationStack {
Group {
switch selectedSection {
case .sources:
SourcesListView()
case .icloud:
iCloudSettingsView()
case .appearance:
AppearanceSettingsView()
case .layoutNavigation:
LayoutNavigationSettingsView()
case .playback:
PlaybackSettingsView()
case .playerControls:
PlayerControlsSettingsView()
case .notifications:
NotificationSettingsView()
case .downloads:
DownloadSettingsView()
case .privacy:
PrivacySettingsView()
case .youtubeEnhancements:
YouTubeEnhancementsSettingsView()
case .advanced:
AdvancedSettingsView()
case .about:
AboutView()
case .none:
Text(String(localized: "settings.placeholder.selectSection"))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
}
}
}
private var macOSSidebarFooter: some View {
VStack(spacing: 6) {
Image("AppIconPreview")
.resizable()
.scaledToFit()
.frame(width: 48, height: 48)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text(verbatim: "Yattee")
.font(.callout)
.fontWeight(.semibold)
Text("\(appVersion) (\(buildNumber))")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.allowsHitTesting(false)
}
#endif
// MARK: - tvOS Settings
#if os(tvOS)
private var tvOSSettings: some View {
List {
if let appEnvironment {
NavigationLink {
SourcesListView()
} label: {
HStack {
Label(String(localized: "sources.title"), systemImage: "server.rack")
Spacer()
if appEnvironment.mediaSourcesManager.hasSourcesNeedingPassword {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.orange)
}
}
}
.accessibilityIdentifier("settings.row.sources")
NavigationLink {
TVSidebarDetailContainer(systemImage: "icloud", title: String(localized: "settings.icloud.title")) { iCloudSettingsView() }
} label: {
HStack {
Label(String(localized: "settings.icloud.title"), systemImage: "icloud")
#if DEBUG
Spacer()
Text(String(localized: "settings.icloud.dev.badge"))
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.orange, in: Capsule())
#endif
}
}
NavigationLink { TVSidebarDetailContainer(systemImage: "paintbrush", title: String(localized: "settings.appearance.sectionTitle")) { AppearanceSettingsView() } } label: {
Label(String(localized: "settings.appearance.sectionTitle"), systemImage: "paintbrush")
}
NavigationLink { TVSidebarDetailContainer(systemImage: "hand.tap", title: String(localized: "settings.layoutNavigation.title")) { LayoutNavigationSettingsView() } } label: {
Label(String(localized: "settings.layoutNavigation.title"), systemImage: "hand.tap")
}
NavigationLink { TVSidebarDetailContainer(systemImage: "rectangle.on.rectangle.angled", title: String(localized: "settings.topShelf.title", defaultValue: "Top Shelf")) { TopShelfSettingsView() } } label: {
Label(String(localized: "settings.topShelf.title", defaultValue: "Top Shelf"), systemImage: "rectangle.on.rectangle.angled")
}
NavigationLink { TVSidebarDetailContainer(systemImage: "play.circle", title: String(localized: "settings.playback.sectionTitle")) { PlaybackSettingsView() } } label: {
Label(String(localized: "settings.playback.sectionTitle"), systemImage: "play.circle")
}
NavigationLink { TVSidebarDetailContainer(systemImage: "hand.raised", title: String(localized: "settings.privacy.title")) { PrivacySettingsView() } } label: {
Label(String(localized: "settings.privacy.title"), systemImage: "hand.raised")
}
if appEnvironment.instancesManager.enabledInstances.contains(where: \.isYouTubeInstance) {
NavigationLink { TVSidebarDetailContainer(systemImage: "puzzlepiece.extension", title: String(localized: "settings.youtubeEnhancements.title")) { YouTubeEnhancementsSettingsView() } } label: {
Label(String(localized: "settings.youtubeEnhancements.title"), systemImage: "puzzlepiece.extension")
}
}
NavigationLink { TVSidebarDetailContainer(systemImage: "gearshape.2", title: String(localized: "settings.advanced.title")) { AdvancedSettingsView() } } label: {
Label(String(localized: "settings.advanced.title"), systemImage: "gearshape.2")
}
NavigationLink { TVSidebarDetailContainer(systemImage: "info.circle", title: String(localized: "settings.about.title")) { AboutView() } } label: {
Label(String(localized: "settings.about.title"), systemImage: "info.circle")
}
}
}
.listStyle(.grouped)
.safeAreaInset(edge: .leading) {
VStack(spacing: 20) {
Spacer()
Image("AppIconPreview")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 46))
Text(verbatim: "Yattee")
.font(.title2)
.fontWeight(.semibold)
Text("\(appVersion) (\(buildNumber))")
.font(.callout)
.foregroundStyle(.secondary)
Spacer()
}
.frame(width: 400)
.allowsHitTesting(false)
}
.accessibilityIdentifier("settings.view")
}
#endif
// MARK: - iOS Settings
#if os(iOS)
private var iOSSettings: some View {
NavigationStack {
List {
if let appEnvironment {
Section {
NavigationLink {
SourcesListView()
} label: {
HStack {
Label(String(localized: "sources.title"), systemImage: "server.rack")
Spacer()
if appEnvironment.mediaSourcesManager.hasSourcesNeedingPassword {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.orange)
}
}
}
.accessibilityIdentifier("settings.row.sources")
}
Section {
NavigationLink {
iCloudSettingsView()
} label: {
HStack {
Label(String(localized: "settings.icloud.title"), systemImage: "icloud")
#if DEBUG
Spacer()
Text(String(localized: "settings.icloud.dev.badge"))
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.orange, in: Capsule())
#endif
}
}
NavigationLink {
AppearanceSettingsView()
} label: {
Label(String(localized: "settings.appearance.sectionTitle"), systemImage: "paintbrush")
}
NavigationLink {
LayoutNavigationSettingsView()
} label: {
Label(String(localized: "settings.layoutNavigation.title"), systemImage: "hand.tap")
}
NavigationLink {
PlaybackSettingsView()
} label: {
Label(String(localized: "settings.playback.sectionTitle"), systemImage: "play.circle")
}
#if os(iOS)
NavigationLink {
PlayerControlsSettingsView()
} label: {
Label(String(localized: "settings.playerControls.title"), systemImage: "slider.horizontal.below.rectangle")
}
NavigationLink {
NotificationSettingsView()
} label: {
Label(String(localized: "settings.notifications.title"), systemImage: "bell.badge")
}
NavigationLink {
DownloadSettingsView()
} label: {
Label(String(localized: "settings.downloads.title"), systemImage: "arrow.down.circle")
}
#endif
NavigationLink {
PrivacySettingsView()
} label: {
Label(String(localized: "settings.privacy.title"), systemImage: "hand.raised")
}
if appEnvironment.instancesManager.enabledInstances.contains(where: \.isYouTubeInstance) {
NavigationLink {
YouTubeEnhancementsSettingsView()
} label: {
Label(String(localized: "settings.youtubeEnhancements.title"), systemImage: "puzzlepiece.extension")
}
}
NavigationLink {
AdvancedSettingsView()
} label: {
Label(String(localized: "settings.advanced.title"), systemImage: "gearshape.2")
}
}
Section {
NavigationLink {
AboutView()
} label: {
Label(String(localized: "settings.about.title"), systemImage: "info.circle")
}
}
Section {
VStack(spacing: 4) {
Text(verbatim: "Yattee")
.font(.headline)
Text("\(appVersion) (\(buildNumber))")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
}
}
.navigationTitle(String(localized: "settings.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if showCloseButton {
sheetCloseToolbarItem(identifier: "settings.doneButton") { dismiss() }
}
}
.accessibilityIdentifier("settings.view")
}
}
#endif
// MARK: - App Info
private var appVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
}
private var buildNumber: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown"
}
}
// MARK: - Settings Sections
enum SettingsSection: String, CaseIterable, Identifiable {
case sources
case icloud
case appearance
case layoutNavigation
case playback
case playerControls
case notifications
case downloads
case privacy
case youtubeEnhancements
case advanced
case about
var id: String { rawValue }
var title: String {
switch self {
case .sources: return String(localized: "sources.title")
case .icloud: return String(localized: "settings.icloud.title")
case .appearance: return String(localized: "settings.appearance.sectionTitle")
case .layoutNavigation: return String(localized: "settings.layoutNavigation.title")
case .playback: return String(localized: "settings.playback.sectionTitle")
case .playerControls: return String(localized: "settings.playerControls.title")
case .notifications: return String(localized: "settings.notifications.title")
case .downloads: return String(localized: "settings.downloads.title")
case .privacy: return String(localized: "settings.privacy.title")
case .youtubeEnhancements: return String(localized: "settings.youtubeEnhancements.title")
case .advanced: return String(localized: "settings.advanced.title")
case .about: return String(localized: "settings.about.title")
}
}
var icon: String {
switch self {
case .sources: return "server.rack"
case .icloud: return "icloud"
case .appearance: return "paintbrush"
case .layoutNavigation: return "hand.tap"
case .playback: return "play.circle"
case .playerControls: return "slider.horizontal.below.rectangle"
case .notifications: return "bell.badge"
case .downloads: return "arrow.down.circle"
case .privacy: return "hand.raised"
case .youtubeEnhancements: return "puzzlepiece.extension"
case .advanced: return "gearshape.2"
case .about: return "info.circle"
}
}
}
#Preview {
SettingsView()
.appEnvironment(.preview)
}