Zweimal in Folge stand „es läuft nicht" gegen „bei mir ist es behoben", ohne dass sich klären ließ, welcher Stand überhaupt auf dem Gerät liegt. Das soll nicht noch einmal passieren. Einstellungen → Diagnose zeigt jetzt: - welcher Server und über welche Verbindung, mit Warnung bei der gedrosselten Plex-Vermittlung - den letzten Abspielversuch: Titel, gewählter Weg, Quelladresse - jeden Schritt mit Zeitabstand zum Start. Der Abstand ist die eigentliche Auskunft: liegt zwischen „Quelle gewählt" und „spielt" eine Minute, hängt es an der Leitung; kommt „spielt" gar nicht, am Motor. - den Zeitpunkt, zu dem der laufende Build entstanden ist Das Build-Datum kommt aus dem Änderungsdatum der eigenen Binärdatei — kein Build-Skript, keine erzeugte Datei, und immer richtig. Der Bericht lässt sich kopieren und enthält keinen Token: die Quelladresse wird ohne Abfrageteil abgelegt, und dort steckt er. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
6.9 KiB
Swift
189 lines
6.9 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(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
|
|
}
|
|
}
|
|
}
|
|
.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"
|
|
}
|
|
}
|
|
}
|