Phase 4 und Teile von Phase 5

Download-Regeln
- Automatisches Löschen nach dem Ansehen, mit Frist statt sofort: wer
  die letzte Szene noch einmal sucht, soll sie nicht schon verloren
  haben. „Nie" ist die Voreinstellung.
- „Halte N ungesehene Folgen offline": wartende und laufende Downloads
  zählen mit, sonst würde bei jedem Durchlauf nachgelegt, bis die ganze
  Serie auf dem Gerät liegt.
- Beides als reine Rechnung ohne Zugriff auf Datenbank oder Dateien —
  bei einer Regel, die Filme löscht, ist Prüfbarkeit keine Formsache.
  13 Tests.

Qualitätswahl beim Download
- transcodedDownloadURL über den Universal-Transcoder mit protocol=http
  statt über die umständliche Sync-API. NOCH NICHT AM SERVER GEPRÜFT:
  offen ist, ob Plex eine Content-Length mitschickt (sonst kein
  Fortschritt und kein Fortsetzen) und ob die Datei durchsuchbar ist.
  Steht so im Quelltext; Original bleibt Voreinstellung.

Erscheinungsbild
- Akzentfarbe: zehn kuratierte Kacheln plus freie Hex-Eingabe. Bei einer
  eigenen Farbe greift die Kontrastkorrektur und sagt es auch — sonst
  wirkt sie wie ein Fehler.
- Vorschaufläche mit Knopf, Symbol und Fortschrittsbalken.

Serveraktionen
- Gesehen-Status umschalten und Sterne setzen über ein Menü auf der
  Detailseite.
- Suchlauf der Mediathek aus der Werkzeugleiste.

115 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 17:40:10 +02:00
co-authored by Claude Opus 5
parent 62158303a2
commit af23372997
10 changed files with 1062 additions and 0 deletions
+73
View File
@@ -1,5 +1,6 @@
import SwiftUI
import FlixyDesign
import FlixyDownloads
import FlixyPlayback
import PlexKit
@@ -11,6 +12,7 @@ import PlexKit
struct SettingsView: View {
@Environment(FlixySession.self) private var session
@Environment(PlaybackSettings.self) private var settings
@Environment(DownloadCoordinator.self) private var downloads
@State private var showsSignOutConfirmation = false
var body: some View {
@@ -54,6 +56,16 @@ struct SettingsView: View {
Toggle("settings.autoPlayNext", isOn: Bindable(settings).autoPlayNext)
}
Section {
NavigationLink {
AppearanceView()
} label: {
Label("appearance.title", systemImage: "paintpalette")
}
}
downloadSection
Section("settings.app") {
LabeledContent("settings.version", value: Branding.version)
}
@@ -80,6 +92,67 @@ struct SettingsView: View {
}
}
/// 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 {