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 { FlixyClipboard.copy(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 { FlixyClipboard.copy(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) .flixyForm() .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("diagnostics.title")) .flixyInlineTitle() } /// 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")) """ } }