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,167 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import MediaKit
|
||||
|
||||
/// Die Mediatheken auf dem iPhone — mit einer Leiste zum Wechseln.
|
||||
///
|
||||
/// **Was hier wegfällt, ist ein Zwischenschritt.** Vorher stand an dieser
|
||||
/// Stelle eine Liste aller Mediatheken; wer Filme sehen wollte, tippte auf
|
||||
/// „Bibliothek", dann auf „Filme", und bekam erst dann ein Raster. Zurück und
|
||||
/// in eine andere hiess: zweimal tippen. Der Zwischenbildschirm hat nie etwas
|
||||
/// erzählt, was die Leiste nicht auch zeigen kann.
|
||||
///
|
||||
/// Auf dem iPad und am Mac gibt es ihn ohnehin nicht — dort steht jede
|
||||
/// Mediathek einzeln in der Seitenleiste, und auf dem Fernseher nebeneinander
|
||||
/// in der oberen Leiste. Das hier ist dieselbe Idee in der Form, die ein
|
||||
/// Telefon hergibt.
|
||||
///
|
||||
/// **Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat
|
||||
/// fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis,
|
||||
/// dessen Einträge zwei Navigationsleisten übereinander tragen. Eine selbst
|
||||
/// gezeichnete Leiste kennt diese Grenze nicht.
|
||||
struct LibraryBrowser: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
|
||||
/// Was gerade gezeigt wird.
|
||||
///
|
||||
/// `nil` heisst „noch nichts gewählt" — dann gilt die erste Mediathek.
|
||||
/// Eine Vorauswahl beim Anlegen ginge nicht: die Mediatheken kommen vom
|
||||
/// Server und stehen beim ersten Aufbau noch nicht fest.
|
||||
@State private var auswahl: Auswahl?
|
||||
|
||||
private enum Auswahl: Hashable {
|
||||
case library(String)
|
||||
case liveTV
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
inhalt
|
||||
.background(Color.haloCanvas)
|
||||
// **Als Sicherheitsrand, nicht als Überzug.** So endet die Liste
|
||||
// über der Leiste, statt dahinter zu verschwinden — und das gilt
|
||||
// auch für die Ladeanzeige und den Fuss „alles geladen".
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
if eintraege.count > 1 { leiste }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var inhalt: some View {
|
||||
switch aktuell {
|
||||
case .library(let id):
|
||||
if let library = session.libraries.first(where: { $0.id == id }) {
|
||||
// **Mit `id`, damit der Wechsel sauber neu aufsetzt.** Ohne
|
||||
// das behielte die Ansicht die Titel der vorigen Mediathek,
|
||||
// bis der Server antwortet — und für einen Augenblick stünden
|
||||
// Filme unter „Serien".
|
||||
LibraryView(library: library).id(library.id)
|
||||
}
|
||||
case .liveTV:
|
||||
LiveTVView()
|
||||
case nil:
|
||||
ContentUnavailableView {
|
||||
Label("libraryHub.empty", systemImage: "books.vertical")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Die Leiste
|
||||
|
||||
private var leiste: some View {
|
||||
// **So breit wie nötig, nicht wie möglich.** Über die ganze Breite
|
||||
// gezogen stand sie linksbündig in einer halbleeren Kapsel — eine
|
||||
// Leiste, die dreimal so lang ist wie ihr Inhalt, sieht aus wie ein
|
||||
// Fehler im Layout.
|
||||
//
|
||||
// `ViewThatFits` nimmt die erste Fassung, die hineinpasst: die
|
||||
// ungescrollte Reihe, solange sie passt, sonst die scrollbare. Damit
|
||||
// ist beides richtig, ohne dass irgendwo eine Zahl stünde, ab wann es
|
||||
// kippt — bei vier Mediatheken passt es, bei acht nicht, und keine
|
||||
// Fassung muss das wissen.
|
||||
ViewThatFits(in: .horizontal) {
|
||||
reihe
|
||||
ScrollView(.horizontal) { reihe }
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
// Dieselbe Fläche wie die Insel auf dem iPad — ein `static`-Bauteil,
|
||||
// kein nachgebautes Aussehen.
|
||||
.background { HaloSidebar.surface(in: .capsule) }
|
||||
.clipShape(.capsule)
|
||||
.shadow(color: .black.opacity(0.45), radius: 18, y: 6)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.bottom, HaloSpacing.snug)
|
||||
}
|
||||
|
||||
private var reihe: some View {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(eintraege, id: \.auswahl) { eintrag in
|
||||
pille(eintrag)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
}
|
||||
|
||||
private func pille(_ eintrag: Eintrag) -> some View {
|
||||
let gewaehlt = eintrag.auswahl == aktuell
|
||||
|
||||
return Button {
|
||||
auswahl = eintrag.auswahl
|
||||
} label: {
|
||||
Label {
|
||||
Text(eintrag.titel).lineLimit(1)
|
||||
} icon: {
|
||||
Image(systemName: eintrag.symbol)
|
||||
}
|
||||
// Kleiner als in der Leiste auf dem iPad: dort stehen drei
|
||||
// Einträge, hier bis zu fünf — und auf einem Telefon.
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(gewaehlt ? Color.haloTextPrimary : Color.haloTextSecondary)
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.background {
|
||||
if gewaehlt { Capsule().fill(Color.haloElevated) }
|
||||
}
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(gewaehlt ? .isSelected : [])
|
||||
}
|
||||
|
||||
// MARK: - Was in der Leiste steht
|
||||
|
||||
private struct Eintrag {
|
||||
let auswahl: Auswahl
|
||||
let titel: String
|
||||
let symbol: String
|
||||
}
|
||||
|
||||
private var eintraege: [Eintrag] {
|
||||
var liste = session.libraries.map {
|
||||
Eintrag(auswahl: .library($0.id), titel: $0.title, symbol: $0.kind.symbol)
|
||||
}
|
||||
// Live-TV wohnte im Zwischenbildschirm und bräuchte sonst ein neues
|
||||
// Zuhause. Hier steht es, wo man es sucht: neben den Mediatheken.
|
||||
if session.supportsLiveTV {
|
||||
liste.append(Eintrag(
|
||||
auswahl: .liveTV,
|
||||
titel: String(localized: "sidebar.liveTV"),
|
||||
symbol: MediaLibrary.Kind.liveTV.symbol
|
||||
))
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
/// Die gültige Auswahl — die gespeicherte, solange es sie noch gibt.
|
||||
///
|
||||
/// **Nicht einfach `auswahl`.** Nach einem Serverwechsel zeigt die
|
||||
/// gespeicherte Kennung auf eine Mediathek, die es nicht mehr gibt; dann
|
||||
/// stünde ein leerer Bildschirm da, ohne dass irgendetwas erklärt, warum.
|
||||
private var aktuell: Auswahl? {
|
||||
if let auswahl, eintraege.contains(where: { $0.auswahl == auswahl }) {
|
||||
return auswahl
|
||||
}
|
||||
return eintraege.first?.auswahl
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user