Schritt drei: anmelden. Die Oberfläche ist neu, das Modell dahinter ist dasselbe wie am Telefon — `JellyfinSignInModel`, unverändert bis auf einen Wert im Initialisierer. **Die eine Verhaltensänderung.** Nach gefundenem Server sprang der Weg bisher immer zu Benutzername und Passwort, mit der Begründung, ein Passwortspeicher fülle beide Felder in einer Sekunde. Vor einem Fernseher stimmt daran kein Wort: es gibt keinen Passwortspeicher, man tippt mit dem Steuerkreuz, und das Gerät, auf dem man den Code bestätigt, liegt sowieso auf dem Sofa. Also `preferring: .quickConnect` — ein Wert im Initialisierer und keine Plattformbedingung mitten in `findServer()`. Welcher Fall gilt, weiß die Oberfläche; das Modell nicht. Der Vorgabewert hält iOS unverändert. Der sechsstellige Code steht in 96 Punkt mit Sperrung: er wird aus drei Metern abgelesen und in ein Telefon getippt. In Fliesstextgrösse wäre das eine Zumutung. Beide Wege bleiben erreichbar — für Server, auf denen Quick Connect abgeschaltet ist, und für den Fall, dass gerade kein zweites Gerät zur Hand ist. **Und der Beweis, dass die geteilte Schicht trägt.** Gegen die Jellyfin-Attrappe im Simulator: die App verbindet sich, lädt die drei Mediatheken und zeigt sie. FlixySession, JellyfinBackend, die Zustandsmaschine der Anmeldung — alles unverändert, alles über Pfadeinträge geteilt, keine Zeile kopiert. Die Startseite ist noch eine Liste von Namen. Sie kommt in Schritt vier; hier steht sie, damit die Wurzel vollständig ist und die Anmeldung für sich prüfbar bleibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
2.9 KiB
Swift
83 lines
2.9 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
|
|
/// Was auf dem Fernseher zu sehen ist.
|
|
///
|
|
/// **Nur der Zustandswechsel, keine Navigation.** Das ist alles, was von
|
|
/// `RootView` auf tvOS übrig bleibt: dort sind es 556 Zeilen Seitenleiste,
|
|
/// Tab-Anpassung, gemessene Statusleistenhöhe und Mini-Player-Übergang, und
|
|
/// nichts davon gibt es hier. Deshalb eine eigene Datei und kein `#if` in der
|
|
/// bestehenden — ein `#if` wäre kein Zweig gewesen, sondern ein zweiter Rumpf
|
|
/// in derselben Datei.
|
|
struct TVRootView: View {
|
|
@Environment(FlixySession.self) private var session
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.flixyCanvas.ignoresSafeArea()
|
|
|
|
switch session.phase {
|
|
case .launching:
|
|
ProgressView()
|
|
|
|
case .signedOut, .authenticating, .choosingServer:
|
|
// Plex bietet die erste Fassung nicht an: die Anmeldung dort
|
|
// läuft über ein Browserfenster, und das gibt es auf tvOS
|
|
// nicht. Alle drei Zustände führen deshalb zur
|
|
// Jellyfin-Anmeldung.
|
|
TVSignInView()
|
|
|
|
case .connecting(let name):
|
|
verbinden(mit: name)
|
|
|
|
case .failed(let meldung):
|
|
fehlgeschlagen(meldung)
|
|
|
|
case .ready:
|
|
TVHomeView()
|
|
}
|
|
}
|
|
.task { await session.restore() }
|
|
}
|
|
|
|
private func verbinden(mit name: String) -> some View {
|
|
VStack(spacing: FlixySpacing.regular) {
|
|
ProgressView()
|
|
Text(name).font(.flixyRowHeader).foregroundStyle(.white)
|
|
}
|
|
}
|
|
|
|
private func fehlgeschlagen(_ meldung: String) -> some View {
|
|
VStack(spacing: FlixySpacing.regular) {
|
|
Text(meldung).font(.flixyBody).foregroundStyle(.white)
|
|
HStack(spacing: FlixySpacing.regular) {
|
|
Button("action.retry") { Task { await session.retry() } }
|
|
Button("action.signOut") { session.signOut() }
|
|
}
|
|
}
|
|
.frame(maxWidth: 900)
|
|
}
|
|
}
|
|
|
|
/// Die Startseite — vorerst nur die Bestätigung, dass die Anmeldung steht.
|
|
///
|
|
/// Sie kommt in Schritt vier; hier steht sie, damit `TVRootView` schon
|
|
/// vollständig ist und die Anmeldung für sich prüfbar bleibt.
|
|
struct TVHomeView: View {
|
|
@Environment(FlixySession.self) private var session
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: FlixySpacing.regular) {
|
|
Text(session.serverName.isEmpty ? Branding.appName : session.serverName).font(.flixyHero)
|
|
ForEach(session.libraries, id: \.id) { mediathek in
|
|
Text(mediathek.title).font(.flixyRowHeader)
|
|
}
|
|
Button("action.signOut") { session.signOut() }
|
|
.padding(.top, FlixySpacing.section)
|
|
}
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(FlixySpacing.screenMargin)
|
|
}
|
|
}
|