Picker-Werte leuchteten weiter in der Standardfarbe, während Schalter und Symbole daneben schon die gewählte trugen. Zwei Ursachen, beide behoben — welche davon im Einzelfall zog, ließ sich nicht nachstellen, weil der Bildschirm hinter der Anmeldung liegt. Erstens stand `.tint` im Scene-Body. Scene-Bodies werten Änderungen an beobachteten Objekten nicht zuverlässig neu aus; nach einem Farbwechsel blieb ein Teil der Oberfläche auf dem alten Wert stehen. Jetzt setzt RootView Tönung und Akzent — eine View wertet zuverlässig neu aus. Zweitens nahm der Vorgabewert der Umgebung blind die Standardfarbe. Listen und Formulare zeichnen ihre Zeilen teilweise in einem eigenen Darstellungskontext, in dem eigene Umgebungswerte verloren gehen; dort erschien dann Mint statt der gewählten Farbe. Der Vorgabewert liest jetzt die gespeicherte Auswahl. Damit das nicht wieder auseinanderläuft, liegt die Speicherung an einer Stelle: AccentOption.stored/store. Vorher hatte FlixyTheme eigene Schlüssel — zwei Kopien derselben Namen driften auseinander, und zwar unbemerkt. Dazu setzen die Formulare die Tönung ausdrücklich, statt sich auf Vererbung zu verlassen. 154 Tests, fünf davon zur Speicherung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
197 lines
7.3 KiB
Swift
197 lines
7.3 KiB
Swift
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<Int64> {
|
|
Binding(
|
|
get: { downloads.storageLimitBytes ?? 0 },
|
|
set: { downloads.storageLimitBytes = $0 > 0 ? $0 : nil }
|
|
)
|
|
}
|
|
|
|
private var deleteAfterBinding: Binding<Double> {
|
|
Binding(
|
|
get: { downloads.rules.deleteWatchedAfter?.seconds ?? -1 },
|
|
set: { downloads.rules.deleteWatchedAfter = $0 >= 0 ? .seconds($0) : nil }
|
|
)
|
|
}
|
|
|
|
private var keepUnwatchedBinding: Binding<Int> {
|
|
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"
|
|
}
|
|
}
|
|
}
|