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,130 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import MediaKit
|
||||
|
||||
/// Eine Mediathek als Raster auf dem Fernseher.
|
||||
///
|
||||
/// **Nachgeladen wird am Fokus, nicht am Erscheinen.** Am Telefon hängt es
|
||||
/// daran, dass eine Kachel sichtbar wird — bei sechzig Kacheln auf einem
|
||||
/// breiten Raster erscheinen die aber alle beim ersten Zeichnen, und das
|
||||
/// Nachladen liefe sofort los statt beim Blättern. Der Fokus steht dagegen
|
||||
/// immer nur an einer Stelle, und genau die ist die Frage.
|
||||
struct TVLibraryView: View {
|
||||
let libraryID: String
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(PlaybackCoordinator.self) private var playback
|
||||
@Environment(QuickPlay.self) private var quickPlay
|
||||
|
||||
@State private var feed: LibraryFeed?
|
||||
@FocusState private var fokus: String?
|
||||
|
||||
/// Sechs Spalten auf 1760 Punkten Nutzbreite: rund 250 Punkte je Plakat,
|
||||
/// aus drei Metern noch erkennbar.
|
||||
private static let spalten = 6
|
||||
|
||||
private var raster: [GridItem] {
|
||||
Array(repeating: GridItem(.flexible(), spacing: HaloSpacing.section), count: Self.spalten)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let feed {
|
||||
inhalt(feed: feed)
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
guard feed == nil,
|
||||
let mediathek = session.libraries.first(where: { $0.id == libraryID })
|
||||
else { return }
|
||||
let frisch = LibraryFeed(library: mediathek)
|
||||
feed = frisch
|
||||
await frisch.reload(from: session.backend)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func inhalt(feed: LibraryFeed) -> some View {
|
||||
switch feed.state {
|
||||
case .loading where feed.items.isEmpty:
|
||||
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
case .failed(let meldung) where feed.items.isEmpty:
|
||||
VStack(spacing: HaloSpacing.regular) {
|
||||
Text(meldung).font(.haloBody).foregroundStyle(.white)
|
||||
Button("action.retry") { Task { await feed.reload(from: session.backend) } }
|
||||
}
|
||||
|
||||
default:
|
||||
raster(feed: feed)
|
||||
}
|
||||
}
|
||||
|
||||
private func raster(feed: LibraryFeed) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.section) {
|
||||
Text(feed.library.title)
|
||||
.font(.haloHero)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
|
||||
LazyVGrid(columns: raster, spacing: HaloSpacing.section) {
|
||||
ForEach(Array(feed.items.enumerated()), id: \.element.id) { stelle, item in
|
||||
TVPosterCard(
|
||||
title: item.rowTitle,
|
||||
subtitle: item.cardSubtitle,
|
||||
artworkURL: session.backend?.artworkURL(
|
||||
path: item.posterArtworkPath, width: 560, height: 840
|
||||
),
|
||||
progress: item.watchProgress
|
||||
) {
|
||||
// **Über QuickPlay, nicht unmittelbar.** Steht im
|
||||
// Raster eine Serie, hat die keine Fassungen; der
|
||||
// direkte Weg schickte ihre eigene Kennung an den
|
||||
// Player und scheiterte mit „keine abspielbare
|
||||
// Datei". QuickPlay löst stattdessen die nächste
|
||||
// ungesehene Folge auf.
|
||||
//
|
||||
// Ein Kontextmenü wie am Telefon gibt es hier
|
||||
// nicht: langes Drücken ist auf der Fernbedienung
|
||||
// keine verlässliche Geste.
|
||||
quickPlay.start(item, backend: session.backend, coordinator: playback)
|
||||
}
|
||||
.focused($fokus, equals: item.id)
|
||||
.onChange(of: fokus) { _, neu in
|
||||
guard neu == item.id,
|
||||
feed.shouldLoadMore(at: stelle, columns: Self.spalten)
|
||||
else { return }
|
||||
Task { await feed.loadMore(from: session.backend) }
|
||||
}
|
||||
}
|
||||
|
||||
// **Die Platzhalter bleiben.** Am Fernseher sind sie
|
||||
// wichtiger als am Telefon: hört das Raster einfach auf,
|
||||
// läuft der Fokus gegen eine unsichtbare Wand. Sie dürfen
|
||||
// ihn aber nicht annehmen, sonst landet er auf einem
|
||||
// grauen Kasten.
|
||||
ForEach(0..<min(feed.pendingCount, Self.spalten * 2), id: \.self) { _ in
|
||||
PlaceholderCard()
|
||||
.focusable(false)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
|
||||
if feed.loadMoreFailed {
|
||||
Button("library.loadMore") {
|
||||
Task { await feed.loadMore(from: session.backend) }
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, HaloSpacing.screenMargin)
|
||||
}
|
||||
.scrollClipDisabled()
|
||||
.scrollIndicators(.never)
|
||||
// Siehe `HaloSpacing.screenMargin`: bildfüllend, Rand im Wert.
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user