Die vier erfundenen Stufen weichen der Leiter, die der Server selbst meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000, 12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt — er rundet dann still auf die nächste, und die Anzeige log. Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus. Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere, was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort. „Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild ausgeblendet. Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt sonst das doppelte. Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören. Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg, er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben. Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler. Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
6.0 KiB
Swift
148 lines
6.0 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import PlexKit
|
|
|
|
/// Was beim letzten Abspielversuch passiert ist.
|
|
///
|
|
/// Gedacht als Antwort auf „es läuft nicht": hier steht, welche Engine gewählt
|
|
/// wurde, über welche Verbindung, und wie lange jeder Schritt gedauert hat.
|
|
/// Ein Screenshot davon sagt mehr als jede Beschreibung.
|
|
struct DiagnosticsView: View {
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(\.flixyAccent) private var accent
|
|
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
|
@State private var liveTVStructure: String?
|
|
@State private var isInspectingLiveTV = false
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("diagnostics.connection") {
|
|
if let server = session.server {
|
|
LabeledContent("settings.serverName", value: server.name)
|
|
LabeledContent("settings.address", value: server.connection.uri)
|
|
.font(.flixyCaption)
|
|
|
|
if server.connection.relay {
|
|
// Der häufigste Grund dafür, dass nichts anläuft. Ohne
|
|
// Hinweis sucht man den Fehler in der App.
|
|
Label("diagnostics.relayWarning", systemImage: "exclamationmark.triangle")
|
|
.font(.flixyCaption)
|
|
.foregroundStyle(Color.flixyDestructive)
|
|
}
|
|
} else {
|
|
Text("diagnostics.noServer").foregroundStyle(Color.flixyTextMuted)
|
|
}
|
|
}
|
|
|
|
if let attempt = diagnostics.lastAttempt {
|
|
Section("diagnostics.lastAttempt") {
|
|
LabeledContent("diagnostics.title.label", value: attempt.title)
|
|
LabeledContent("diagnostics.method", value: attempt.method)
|
|
LabeledContent("diagnostics.connectionUsed", value: attempt.connection)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("diagnostics.source")
|
|
.font(.flixyCaption)
|
|
.foregroundStyle(Color.flixyTextMuted)
|
|
Text(attempt.sourceDescription)
|
|
.font(.system(.caption2, design: .monospaced))
|
|
.foregroundStyle(Color.flixyTextSecondary)
|
|
}
|
|
}
|
|
|
|
Section("diagnostics.steps") {
|
|
ForEach(Array(attempt.steps.enumerated()), id: \.offset) { _, step in
|
|
Text(step)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(Color.flixyTextSecondary)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button {
|
|
UIPasteboard.general.string = report(for: attempt)
|
|
} label: {
|
|
Label("diagnostics.copy", systemImage: "doc.on.doc")
|
|
}
|
|
} footer: {
|
|
Text("diagnostics.copyHint")
|
|
}
|
|
} else {
|
|
Section("diagnostics.lastAttempt") {
|
|
Text("diagnostics.nothingYet").foregroundStyle(Color.flixyTextMuted)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button {
|
|
Task { await inspectLiveTV() }
|
|
} label: {
|
|
if isInspectingLiveTV {
|
|
ProgressView().controlSize(.small)
|
|
} else {
|
|
Label("diagnostics.liveTV", systemImage: "antenna.radiowaves.left.and.right")
|
|
}
|
|
}
|
|
.disabled(isInspectingLiveTV)
|
|
|
|
if let liveTVStructure {
|
|
Text(liveTVStructure)
|
|
.font(.system(.caption2, design: .monospaced))
|
|
.foregroundStyle(Color.flixyTextSecondary)
|
|
.textSelection(.enabled)
|
|
|
|
Button {
|
|
UIPasteboard.general.string = liveTVStructure
|
|
} label: {
|
|
Label("diagnostics.copy", systemImage: "doc.on.doc")
|
|
}
|
|
}
|
|
} header: {
|
|
Text("sidebar.liveTV")
|
|
} footer: {
|
|
Text("diagnostics.liveTVHint")
|
|
}
|
|
|
|
Section("settings.app") {
|
|
LabeledContent("settings.version", value: Branding.version)
|
|
LabeledContent("settings.build", value: Branding.buildStamp)
|
|
}
|
|
}
|
|
// 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("diagnostics.title"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
/// Fragt den Server, wie seine Antwort auf `/livetv/dvrs` aufgebaut ist.
|
|
private func inspectLiveTV() async {
|
|
guard let server = session.server else { return }
|
|
isInspectingLiveTV = true
|
|
defer { isInspectingLiveTV = false }
|
|
liveTVStructure = await server.liveTVStructure()
|
|
}
|
|
|
|
/// Der Bericht als Text zum Weitergeben.
|
|
///
|
|
/// Enthält bewusst keinen Token: die Quelladresse ist schon ohne
|
|
/// Abfrageteil abgelegt, und der Servername verrät nichts Vertrauliches.
|
|
private func report(for attempt: PlaybackDiagnostics.Attempt) -> String {
|
|
"""
|
|
Flixy \(Branding.version) · Build \(Branding.buildStamp)
|
|
Verbindung: \(attempt.connection)
|
|
Titel: \(attempt.title)
|
|
Weg: \(attempt.method)
|
|
Quelle: \(attempt.sourceDescription)
|
|
|
|
\(attempt.steps.joined(separator: "\n"))
|
|
"""
|
|
}
|
|
}
|