Files
yattee/Yattee/Views/Settings/SettingsView.swift
Arkadiusz Fal 9e2b0fa095 Extend opaque background to settings and other pushed pages on macOS
The NavigationStack-level background wasn't enough: macOS draws its own
translucent material behind pushed navigation pages, and Form/List
containers draw a translucent scroll background on top of anything
placed behind them, so several pages kept the wallpaper-tinted look.

Bake the opaque background into SettingsFormContainer (covers all pages
built on it, pushed or not) and add opaqueSettingsFormBackground(),
which hides the container's scroll background before applying the
opaque one, to every macOS-reachable Form/List settings page: player
controls and its sub-editors, sidebar settings, edit source, legacy
data import, subscription/playlist import, customize home and its
shortcut style page, and the log viewer. Contributors, translators and
the remote control device page get the plain opaque background.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
2026-07-07 22:16:09 +02:00

412 lines
17 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)
}
}
}
// Covers all section pages and their pushed sub-pages, which
// otherwise sit on the translucent wallpaper-tinted material.
.opaqueWindowBackground()
}
}
}
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)
}