import SwiftUI import FlixyDesign import FlixyDownloads import FlixyPlayback import PlexKit /// Die Einstellungen. /// /// In Phase 1 bewusst schmal: Serverangaben und Abmelden. Akzentfarbe, /// Qualitätsregler und Download-Einstellungen kommen mit den Phasen, in denen /// es das Geregelte auch gibt. struct SettingsView: View { @Environment(FlixySession.self) private var session @Environment(\.flixyAccent) private var accent @Environment(PlaybackSettings.self) private var settings @Environment(DownloadCoordinator.self) private var downloads @State private var showsSignOutConfirmation = false var body: some View { Form { if let server = session.server { Section("settings.server") { LabeledContent("settings.serverName", value: server.name) LabeledContent("settings.connection", value: connectionLabel(server.connection)) LabeledContent("settings.address", value: server.connection.uri) .font(.flixyCaption) .textSelection(.enabled) } Section("settings.libraries") { ForEach(session.libraries) { library in LabeledContent(library.title, value: library.type.capitalized) } } } Section { Picker("settings.qualityWifi", selection: Bindable(settings).wifiQuality) { ForEach(PlaybackSettings.Quality.allCases) { quality in Text(quality.label).tag(quality) } } Picker("settings.qualityCellular", selection: Bindable(settings).cellularQuality) { ForEach(PlaybackSettings.Quality.allCases) { quality in Text(quality.label).tag(quality) } } } header: { Text("settings.quality") } footer: { Text("settings.qualityExplanation") } Section("settings.playback") { Toggle("settings.autoSkipIntro", isOn: Bindable(settings).autoSkipIntro) Toggle("settings.autoSkipCredits", isOn: Bindable(settings).autoSkipCredits) Toggle("settings.autoPlayNext", isOn: Bindable(settings).autoPlayNext) } Section { NavigationLink { AppearanceView() } label: { Label("appearance.title", systemImage: "paintpalette") } NavigationLink { HomeLayoutView() } label: { Label("homeLayout.title", systemImage: "rectangle.stack") } NavigationLink { DiagnosticsView() } label: { Label("diagnostics.title", systemImage: "stethoscope") } } downloadSection Section("settings.app") { LabeledContent("settings.version", value: Branding.version) LabeledContent("settings.build", value: Branding.buildStamp) } Section { Button("action.signOut", role: .destructive) { showsSignOutConfirmation = true } } } // Ausdrücklich, nicht nur geerbt. // // Listen und Formulare zeichnen ihre Zeilen teilweise in einem // eigenen Darstellungskontext, in dem eigene Umgebungswerte verloren // gehen können — sichtbar an Picker-Werten, die in der Standardfarbe // leuchteten, während die Schalter daneben schon die gewählte trugen. .tint(accent) .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.settings")) .navigationBarTitleDisplayMode(.inline) .confirmationDialog( Text("settings.signOutConfirm"), isPresented: $showsSignOutConfirmation, titleVisibility: .visible ) { Button("action.signOut", role: .destructive) { session.signOut() } Button("action.cancel", role: .cancel) {} } message: { Text("settings.signOutExplanation") } } /// Die Download-Einstellungen. /// /// Die Löschfrist steht bewusst mit ausgeschriebenen Fristen da statt mit /// einem Schieberegler: „Nach 7 Tagen" ist eine Entscheidung, die man /// bewusst trifft — beim Löschen von Filmen soll niemand versehentlich /// auf „sofort" rutschen. @ViewBuilder private var downloadSection: some View { @Bindable var downloads = downloads Section { Picker("settings.storageLimit", selection: storageLimitBinding) { Text("settings.storageUnlimited").tag(Int64(0)) ForEach([Int64(5), 10, 20, 30, 50, 100], id: \.self) { gigabytes in Text("\(gigabytes) GB").tag(gigabytes * 1_000_000_000) } } Picker("settings.deleteWatched", selection: deleteAfterBinding) { Text("settings.deleteNever").tag(Double(-1)) Text("settings.deleteImmediately").tag(Double(0)) Text("settings.deleteAfterDay").tag(Duration.afterOneDay.seconds) Text("settings.deleteAfterWeek").tag(Duration.afterOneWeek.seconds) } Picker("settings.keepUnwatched", selection: keepUnwatchedBinding) { Text("settings.keepOff").tag(0) ForEach([1, 2, 3, 5], id: \.self) { count in Text("settings.keepCount \(count)").tag(count) } } Toggle("downloads.wifiOnly", isOn: $downloads.isWiFiOnly) } header: { Text("sidebar.downloads") } footer: { Text("settings.downloadsExplanation") } } private var storageLimitBinding: Binding { Binding( get: { downloads.storageLimitBytes ?? 0 }, set: { downloads.storageLimitBytes = $0 > 0 ? $0 : nil } ) } private var deleteAfterBinding: Binding { Binding( get: { downloads.rules.deleteWatchedAfter?.seconds ?? -1 }, set: { downloads.rules.deleteWatchedAfter = $0 >= 0 ? .seconds($0) : nil } ) } private var keepUnwatchedBinding: Binding { Binding( get: { downloads.rules.keepUnwatchedEpisodes ?? 0 }, set: { downloads.rules.keepUnwatchedEpisodes = $0 > 0 ? $0 : nil } ) } /// Zeigt, über welchen Weg die Verbindung läuft — beim Suchen nach /// Ruckeln ist das die erste Frage. private func connectionLabel(_ connection: PlexConnection) -> String { if connection.local { return String(localized: "settings.connectionLocal") } if connection.relay { return String(localized: "settings.connectionRelay") } return String(localized: "settings.connectionRemote") } } extension PlaybackSettings.Quality { var label: LocalizedStringKey { switch self { case .original: "quality.original" case .high: "quality.high" case .medium: "quality.medium" case .low: "quality.low" case .minimal: "quality.minimal" } } }