Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,158 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
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(HaloSession.self) private var session
|
||||
|
||||
@State private var model: JellyfinSignInModel?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.haloCanvas.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: HaloSpacing.section) {
|
||||
Text(Branding.appName).font(.haloHero)
|
||||
Text("signIn.tagline").font(.haloBody).foregroundStyle(Color.haloTextMuted)
|
||||
|
||||
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(.haloBody).foregroundStyle(Color.haloDestructive)
|
||||
}
|
||||
}
|
||||
.padding(HaloSpacing.section)
|
||||
}
|
||||
|
||||
// MARK: - Die vier Schritte
|
||||
|
||||
private func adresse(model: JellyfinSignInModel) -> some View {
|
||||
@Bindable var model = model
|
||||
|
||||
return VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
Text("signIn.jellyfin.addressPrompt").font(.haloRowHeader)
|
||||
// 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)
|
||||
.haloPlainEntry(isAddress: true)
|
||||
Text("signIn.jellyfin.addressHint")
|
||||
.font(.haloCaption).foregroundStyle(Color.haloTextMuted)
|
||||
Button("signIn.jellyfin.connect") {
|
||||
Task { await model.findServer() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func codeAnzeigen(model: JellyfinSignInModel, code: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
if let name = model.serverName {
|
||||
Text(name).font(.haloRowHeader)
|
||||
}
|
||||
// **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(.haloBody)
|
||||
Label("signIn.jellyfin.waitingForApproval", systemImage: "hourglass")
|
||||
.font(.haloCaption).foregroundStyle(Color.haloTextMuted)
|
||||
|
||||
HStack(spacing: HaloSpacing.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: HaloSpacing.regular) {
|
||||
if let name = model.serverName {
|
||||
Text(name).font(.haloRowHeader)
|
||||
}
|
||||
TextField("signIn.jellyfin.userName", text: $model.userName)
|
||||
.haloPlainEntry()
|
||||
SecureField("signIn.jellyfin.password", text: $model.password)
|
||||
Text("signIn.jellyfin.passwordNote")
|
||||
.font(.haloCaption).foregroundStyle(Color.haloTextMuted)
|
||||
|
||||
HStack(spacing: HaloSpacing.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: HaloSpacing.regular) {
|
||||
ProgressView()
|
||||
Text(text).font(.haloBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user