import SwiftUI import FlixyDesign /// Der Anmeldebildschirm. /// /// Zwei Wege, weil es zwei Serverarten gibt. **Plex** öffnet seine eigene /// Anmeldemaske in einem Fenster innerhalb der App; das Passwort wird dort /// eingegeben und erreicht Flixy nie. **Jellyfin** hat kein Verzeichnis wie /// plex.tv — dort wird die Adresse des eigenen Servers eingetippt und, wann /// immer der Server es anbietet, ein Code bestätigt statt eines Passworts. /// /// Die beiden Wege sind absichtlich nicht zu einem zusammengefasst. Sie haben /// nichts gemeinsam außer dem Ziel, und der Versuch, das zu verstecken, würde /// beide verschlechtern. struct SignInView: View { let phase: FlixySession.Phase /// Der Code der laufenden Plex-Anmeldung, sobald er gezeigt werden soll. let manualCode: String? let onSignIn: () -> Void let onCancel: () -> Void @Environment(\.flixyAccent) private var accent @State private var showsJellyfinSignIn = false private var isAuthenticating: Bool { if case .authenticating = phase { return true } return false } var body: some View { VStack(spacing: FlixySpacing.section) { Spacer() VStack(spacing: FlixySpacing.regular) { Image(systemName: "play.rectangle.on.rectangle") .font(.system(size: 52)) .foregroundStyle(accent) Text(Branding.appName) .font(.flixyHero) .foregroundStyle(Color.flixyTextPrimary) Text("signIn.tagline") .font(.flixyBody) .foregroundStyle(Color.flixyTextSecondary) .multilineTextAlignment(.center) } Spacer() VStack(spacing: FlixySpacing.regular) { Button(action: onSignIn) { Group { if isAuthenticating { ProgressView().tint(Color.flixyCanvas) } else { Text("signIn.withPlex").font(.flixyRowHeader) } } .frame(maxWidth: .infinity) .padding(.vertical, FlixySpacing.regular) } .buttonStyle(.borderedProminent) .tint(accent) .disabled(isAuthenticating) Button { showsJellyfinSignIn = true } label: { Text("signIn.withJellyfin") .font(.flixyRowHeader) .frame(maxWidth: .infinity) .padding(.vertical, FlixySpacing.regular) } .buttonStyle(.bordered) .tint(Color.flixyTextSecondary) .disabled(isAuthenticating) if let manualCode { manualFallback(code: manualCode) } else { Text("signIn.privacy") .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) .multilineTextAlignment(.center) } } Spacer().frame(height: FlixySpacing.section) } .padding(.horizontal, FlixySpacing.section) .frame(maxWidth: 420) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.flixyCanvas) .sheet(isPresented: $showsJellyfinSignIn) { JellyfinSignInView() } } /// Der Ausweg, wenn die Browser-Seite den Code nicht verknüpft. /// /// Das passiert, wenn man dort schon bei Plex angemeldet ist: die Seite /// springt weiter, ohne zu verknüpfen, und die App wartete vergeblich. /// Der Code gilt weiter — abtippen führt immer zum Ziel. @ViewBuilder private func manualFallback(code: String) -> some View { VStack(spacing: FlixySpacing.regular) { Divider().overlay(Color.flixySeparator) Text("signIn.manualHint") .font(.flixyCaption) .foregroundStyle(Color.flixyTextSecondary) .multilineTextAlignment(.center) Text(code) .font(.system(.title, design: .monospaced, weight: .semibold)) .tracking(6) .foregroundStyle(Color.flixyTextPrimary) .textSelection(.enabled) .padding(.vertical, FlixySpacing.snug) .frame(maxWidth: .infinity) .background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.control)) Button("action.cancel", action: onCancel) .font(.flixyCaption) .foregroundStyle(Color.flixyTextSecondary) } .transition(.opacity) } }