Files
yattee/Yattee/Views/Settings/SettingsView.swift
Arkadiusz Fal 2a49a5db68 Use production CloudKit environment for Xcode builds
Development-signed builds now opt into the Production CloudKit
environment via com.apple.developer.icloud-container-environment,
driven by a CLOUDKIT_ENV build setting. A new Debug-CKDev configuration
and "Yattee (CloudKit Dev)" scheme switch to the Development
environment for schema work.

The DEV badge and banner in iCloud settings now reflect the actual
environment (YatteeCloudKitEnvironment Info.plist key) instead of
#if DEBUG, and the sync engine logs the environment at startup.
2026-10-23 21:47:34 +02:00

465 lines
19 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)
/// Accounts stays out of the list until a source that can hold one exists
/// (or an account survives its source and needs cleaning up).
private var visibleSections: [SettingsSection] {
SettingsSection.allCases.filter {
$0 != .accounts || AccountsListView.showsSettingsEntry(in: appEnvironment)
}
}
private var macOSSettings: some View {
NavigationSplitView {
VStack(spacing: 0) {
List(visibleSections, selection: $selectedSection) { section in
HStack {
Label(section.title, systemImage: section.icon)
if section == .accounts, AccountsListView.needsAttention(in: appEnvironment) {
Spacer()
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.orange)
}
}
.tag(section)
}
.listStyle(.sidebar)
Divider()
macOSSidebarFooter
}
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
} detail: {
if appEnvironment != nil {
NavigationStack {
Group {
switch selectedSection {
case .sources:
SourcesListView()
case .accounts:
AccountsListView()
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")
if AccountsListView.showsSettingsEntry(in: appEnvironment) {
NavigationLink {
AccountsListView()
} label: {
HStack {
Label(String(localized: "accounts.title"), systemImage: "person.crop.circle")
Spacer()
if AccountsListView.needsAttention(in: appEnvironment) {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.orange)
}
}
}
.accessibilityIdentifier("settings.row.accounts")
}
NavigationLink {
TVSidebarDetailContainer(systemImage: "icloud", title: String(localized: "settings.icloud.title")) { iCloudSettingsView() }
} label: {
HStack {
Label(String(localized: "settings.icloud.title"), systemImage: "icloud")
if AppIdentifiers.isCloudKitDevelopmentEnvironment {
Spacer()
Text(String(localized: "settings.icloud.dev.badge"))
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.orange, in: Capsule())
}
}
}
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")
if AccountsListView.showsSettingsEntry(in: appEnvironment) {
NavigationLink {
AccountsListView()
} label: {
HStack {
Label(String(localized: "accounts.title"), systemImage: "person.crop.circle")
Spacer()
if AccountsListView.needsAttention(in: appEnvironment) {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.orange)
}
}
}
.accessibilityIdentifier("settings.row.accounts")
}
}
Section {
NavigationLink {
iCloudSettingsView()
} label: {
HStack {
Label(String(localized: "settings.icloud.title"), systemImage: "icloud")
if AppIdentifiers.isCloudKitDevelopmentEnvironment {
Spacer()
Text(String(localized: "settings.icloud.dev.badge"))
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.orange, in: Capsule())
}
}
}
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 accounts
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 .accounts: return String(localized: "accounts.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 .accounts: return "person.crop.circle"
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)
}