import SwiftUI import FlaroDesign import FlaroServerKit /// Die Anmeldung an einem Flaro-Server, am Fernseher. /// /// Dasselbe Modell wie am Telefon, nur mit `preferring: .code`: sechs Ziffern /// auf dem Bildschirm, bestätigt am Telefon. Das Passwort bleibt der Rückfall /// — für den Fall, dass gerade kein angemeldetes Gerät zur Hand ist. struct TVFlaroSignInView: View { @Environment(FlaroSession.self) private var session let zurueck: () -> Void @State private var model: FlaroSignInModel? var body: some View { Group { if let model { inhalt(for: model) } else { ProgressView() } } .task { guard model == nil else { return } let frisch = FlaroSignInModel( identity: session.flaroDeviceIdentity, preferring: .code) frisch.onAuthenticated = { client, konto, token in Task { await session.adoptFlaro(client: client, account: konto, token: token) } } model = frisch } .onDisappear { model?.cancel() } } @ViewBuilder private func inhalt(for model: FlaroSignInModel) -> some View { @Bindable var model = model VStack(alignment: .leading, spacing: FlaroSpacing.section) { Text(Branding.appName).font(.flaroHero) Text("signIn.flaro.title").font(.flaroBody).foregroundStyle(Color.flaroTextMuted) switch model.step { case .address: adresse(model: model) case .checking: warten("signIn.flaro.searching") case .code(let code): codeAnzeigen(model: model, code: code) case .credentials: zugangsdaten(model: model) case .signingIn: warten("signIn.flaro.signingIn") } if let fehler = model.errorMessage { Text(fehler).font(.flaroBody).foregroundStyle(Color.flaroDestructive) } } .padding(FlaroSpacing.section) } private func adresse(model: FlaroSignInModel) -> some View { @Bindable var model = model return VStack(alignment: .leading, spacing: FlaroSpacing.regular) { Text("signIn.flaro.addressPrompt").font(.flaroRowHeader) TextField("signIn.flaro.addressPlaceholder", text: $model.address) .flaroPlainEntry(isAddress: true) Text("signIn.flaro.addressHint") .font(.flaroCaption).foregroundStyle(Color.flaroTextMuted) HStack(spacing: FlaroSpacing.regular) { Button("signIn.flaro.findServer") { Task { await model.findServer() } } Button("action.back", action: zurueck) } } } private func codeAnzeigen(model: FlaroSignInModel, code: String) -> some View { VStack(alignment: .leading, spacing: FlaroSpacing.regular) { if let name = model.serverName { Text(name).font(.flaroRowHeader) } // **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.flaro.codeHint").font(.flaroBody) Label("signIn.flaro.waitingForApproval", systemImage: "hourglass") .font(.flaroCaption).foregroundStyle(Color.flaroTextMuted) HStack(spacing: FlaroSpacing.regular) { Button("signIn.flaro.usePassword") { model.useCredentials() } Button("signIn.flaro.changeAddress") { model.changeAddress() } } } } private func zugangsdaten(model: FlaroSignInModel) -> some View { @Bindable var model = model return VStack(alignment: .leading, spacing: FlaroSpacing.regular) { if let name = model.serverName { Text(name).font(.flaroRowHeader) } TextField("signIn.userName", text: $model.userName) .flaroPlainEntry() SecureField("signIn.password", text: $model.password) HStack(spacing: FlaroSpacing.regular) { Button("signIn.flaro.signIn") { Task { await model.signIn() } } Button("signIn.flaro.useCode") { Task { await model.beginCode() } } Button("signIn.flaro.changeAddress") { model.changeAddress() } } } } private func warten(_ text: LocalizedStringKey) -> some View { HStack(spacing: FlaroSpacing.regular) { ProgressView() Text(text).font(.flaroBody) } } }