import SwiftUI import FlixyDesign import JellyfinKit /// Die Anmeldung am Fernseher. /// /// **Dieselben vier Schritte wie am Telefon, andere Reihenfolge.** Das Modell /// `JellyfinSignInModel` ist unverändert dasselbe — nur bekommt es hier /// `preferring: .quickConnect`. Vor einem Fernseher ist der Code der bequemere /// Weg: es gibt keinen Passwortspeicher, getippt wird mit dem Steuerkreuz, und /// das Gerät, auf dem man den Code bestätigt, liegt sowieso auf dem Sofa. /// /// Der Weg über Benutzername und Passwort bleibt erreichbar — für Server, auf /// denen Quick Connect abgeschaltet ist. struct TVSignInView: View { @Environment(FlixySession.self) private var session @State private var model: JellyfinSignInModel? var body: some View { ZStack { Color.flixyCanvas.ignoresSafeArea() if let model { inhalt(for: model) // Eine Spalte, nicht die volle Breite: 1760 Punkte Zeile // liest sich aus drei Metern nicht mehr als Zeile. .frame(maxWidth: 900) } else { ProgressView() } } .task { // Erst hier, nicht im Eigenschaftswert: das Modell braucht die // Gerätekennung der Sitzung. Baut es sich eine eigene, steht die // App nach dem Anmelden ein zweites Mal in der Geräteliste. guard model == nil else { return } let frisch = JellyfinSignInModel( identity: session.jellyfinDeviceIdentity, preferring: .quickConnect ) frisch.onAuthenticated = { client, account, serverName in Task { await session.adoptJellyfin( client: client, account: account, serverName: serverName ) } } model = frisch } .onDisappear { model?.cancel() } } @ViewBuilder private func inhalt(for model: JellyfinSignInModel) -> some View { @Bindable var model = model VStack(alignment: .leading, spacing: FlixySpacing.section) { Text(Branding.appName).font(.flixyHero) Text("signIn.tagline").font(.flixyBody).foregroundStyle(Color.flixyTextMuted) switch model.step { case .address: adresse(model: model) case .checking: warten("signIn.jellyfin.searching") case .quickConnect(let code): codeAnzeigen(model: model, code: code) case .credentials: zugangsdaten(model: model) case .signingIn: warten("signIn.jellyfin.signingIn") } if let fehler = model.errorMessage { Text(fehler).font(.flixyBody).foregroundStyle(Color.flixyDestructive) } } .padding(FlixySpacing.section) } // MARK: - Die vier Schritte private func adresse(model: JellyfinSignInModel) -> some View { @Bindable var model = model return VStack(alignment: .leading, spacing: FlixySpacing.regular) { Text("signIn.jellyfin.addressPrompt").font(.flixyRowHeader) // Auf tvOS öffnet ein Textfeld beim Fokussieren die // Bildschirmtastatur ganzseitig — deshalb steht hier nur das Feld // und keine Maske drumherum. TextField("signIn.jellyfin.addressPlaceholder", text: $model.address) .flixyPlainEntry(isAddress: true) Text("signIn.jellyfin.addressHint") .font(.flixyCaption).foregroundStyle(Color.flixyTextMuted) Button("signIn.jellyfin.connect") { Task { await model.findServer() } } } } private func codeAnzeigen(model: JellyfinSignInModel, code: String) -> some View { VStack(alignment: .leading, spacing: FlixySpacing.regular) { if let name = model.serverName { Text(name).font(.flixyRowHeader) } // **Der Code ist hier der Inhalt, nicht eine Beschriftung.** // Er wird aus drei Metern abgelesen und in ein Telefon getippt; // in Fliesstextgrösse ist das eine Zumutung. Text(code) .font(.system(size: 96, weight: .bold, design: .monospaced)) .kerning(12) .foregroundStyle(.white) Text("signIn.jellyfin.quickConnectHint").font(.flixyBody) Label("signIn.jellyfin.waitingForApproval", systemImage: "hourglass") .font(.flixyCaption).foregroundStyle(Color.flixyTextMuted) HStack(spacing: FlixySpacing.regular) { Button("signIn.jellyfin.usePassword") { model.useCredentials() } Button("signIn.jellyfin.otherAddress") { model.startOver() } } } } private func zugangsdaten(model: JellyfinSignInModel) -> some View { @Bindable var model = model return VStack(alignment: .leading, spacing: FlixySpacing.regular) { if let name = model.serverName { Text(name).font(.flixyRowHeader) } TextField("signIn.jellyfin.userName", text: $model.userName) .flixyPlainEntry() SecureField("signIn.jellyfin.password", text: $model.password) Text("signIn.jellyfin.passwordNote") .font(.flixyCaption).foregroundStyle(Color.flixyTextMuted) HStack(spacing: FlixySpacing.regular) { Button("signIn.jellyfin.submit") { Task { await model.signInWithPassword() } } if model.offersQuickConnect { Button("signIn.jellyfin.useCode") { Task { await model.beginQuickConnect() } } } Button("signIn.jellyfin.otherAddress") { model.startOver() } } } } private func warten(_ text: LocalizedStringKey) -> some View { HStack(spacing: FlixySpacing.regular) { ProgressView() Text(text).font(.flixyBody) } } }