Files
Flaro/Flixy/Views/SignIn/SignInView.swift
T
GuidoandClaude Opus 5 9a69df4635 Phase 1: Anmeldung, Bibliothek, Detailseiten, Hybrid-Player
Die Datenschicht:
- plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund
- Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor
  Vermittlung), mit Abgleich der Server-Kennung gegen veraltete
  plex.direct-Adressen
- PlexServer als einzige Stelle, an der die App Endpunkte kennt:
  Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten,
  Staffeln/Folgen, Suche

Die Oberfläche:
- Startseite mit Aufmacher und den Reihen vom Server
- Mediathek als Raster oder Liste, mit Filter- und Sortierblatt
- Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung,
  Fassungsauswahl und aufklappbaren technischen Angaben
- Suche mit gemerkten Anfragen
- Vollbildplayer mit eigenem Chrome

Der Player:
- AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll
- VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test`
  in FlixyKit nicht das 920-MB-Binärpaket mitzieht

Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf,
dass `strong=true` einen dreißigstelligen Code liefert, der sich bei
plex.tv/link nicht eintippen lässt — jetzt der kurze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:46:28 +02:00

108 lines
3.7 KiB
Swift

import SwiftUI
import FlixyDesign
import PlexKit
/// Der Anmeldebildschirm.
///
/// Zeigt den Code, den Plex vergeben hat, und schickt in den Browser. Die App
/// bekommt Benutzername und Passwort nie zu sehen — sie fragt nur regelmäßig
/// nach, ob der Code bestätigt wurde.
struct SignInView: View {
let phase: FlixySession.Phase
let clientIdentifier: String
let onSignIn: () -> Void
let onCancel: () -> Void
@Environment(\.openURL) private var openURL
@Environment(\.flixyAccent) private var accent
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()
if case .awaitingAuthorization(let pin) = phase {
awaitingAuthorization(pin: pin)
} else {
Button(action: onSignIn) {
Text("signIn.action")
.font(.flixyRowHeader)
.frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular)
}
.buttonStyle(.borderedProminent)
.tint(accent)
}
Spacer().frame(height: FlixySpacing.section)
}
.padding(.horizontal, FlixySpacing.section)
.frame(maxWidth: 420)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
}
@ViewBuilder
private func awaitingAuthorization(pin: PlexPin) -> some View {
VStack(spacing: FlixySpacing.loose) {
Text("signIn.codeHint")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
.multilineTextAlignment(.center)
Text(pin.code)
// Feste Zeichenbreite, damit der Code beim Abtippen nicht
// springt und Ziffern nicht mit Buchstaben verschwimmen.
.font(.system(.largeTitle, design: .monospaced, weight: .semibold))
.tracking(6)
.foregroundStyle(Color.flixyTextPrimary)
.textSelection(.enabled)
.padding(.vertical, FlixySpacing.regular)
.frame(maxWidth: .infinity)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.sheet))
Button {
if let url = pin.authorizationURL(
clientIdentifier: clientIdentifier,
product: Branding.plexProduct
) {
openURL(url)
}
} label: {
Label("signIn.openBrowser", systemImage: "safari")
.frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular)
}
.buttonStyle(.borderedProminent)
.tint(accent)
HStack(spacing: FlixySpacing.snug) {
ProgressView().controlSize(.small)
Text("signIn.waiting")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextMuted)
}
Button("action.cancel", action: onCancel)
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
}
}
}