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,112 @@
|
||||
// **Die ganze Datei nur für den Fernseher.**
|
||||
//
|
||||
// Sie liegt im geteilten Ordner, weil `Halo/Session/` von beiden Zielen
|
||||
// übersetzt wird — und `TVServices` gibt es auf iOS nicht. Ein `#if` um den
|
||||
// einen Import genügte dabei nicht: der Abhängigkeits-Sucher von Clang prüft
|
||||
// ihn trotzdem und bricht mit „Unable to resolve module dependency:
|
||||
// TVServices" ab, ohne den Fernseher zu erwähnen. Dieselbe Falle wie bei
|
||||
// siehe `PlaybackSettings`.
|
||||
#if os(tvOS)
|
||||
import Foundation
|
||||
import MediaKit
|
||||
import TVServices
|
||||
|
||||
/// Legt für das Regal über der App hin, was es zeigen soll.
|
||||
///
|
||||
/// **Beim Laden der Startseite, nicht auf einem Zeitgeber.** Das Regal wird
|
||||
/// gezeichnet, wenn jemand im Menü über der App steht — und dann ist die App
|
||||
/// nicht gestartet. Was dort steht, ist also immer das, was beim letzten
|
||||
/// Öffnen zu sehen war. Ein Zeitgeber änderte daran nichts und hielte nur die
|
||||
/// App wach.
|
||||
@MainActor
|
||||
enum TopShelfWriter {
|
||||
/// Die App-Gruppe, die App und Erweiterung teilen.
|
||||
static let group = "group.com.scarriffleservices.halo"
|
||||
|
||||
/// Die Kante, in der das Regal Plakate zeigt.
|
||||
///
|
||||
/// **Apples Mass für die doppelte Auflösung**, und für beide Stufen
|
||||
/// dasselbe Bild. Ein Apple TV zeichnet das Regal ohnehin doppelt
|
||||
/// aufgelöst; zwei Adressen abzulegen hiesse, dem Server zwei Grössen
|
||||
/// desselben Plakats abzuverlangen, damit eine davon nie gebraucht wird.
|
||||
private static let posterWidth = 666
|
||||
private static let posterHeight = 1000
|
||||
|
||||
static func update(
|
||||
rows: [MediaRowGroup],
|
||||
backend: (any MediaBackend)?
|
||||
) {
|
||||
guard let backend else { return }
|
||||
|
||||
let abschnitte = rows
|
||||
.prefix(TopShelfSnapshot.sectionLimit)
|
||||
.map { reihe in
|
||||
TopShelfSnapshot.Section(
|
||||
id: reihe.id,
|
||||
title: reihe.title,
|
||||
entries: reihe.items
|
||||
.prefix(TopShelfSnapshot.entryLimit)
|
||||
.map { item in
|
||||
TopShelfSnapshot.Entry(
|
||||
id: item.id,
|
||||
title: item.rowTitle,
|
||||
subtitle: item.cardSubtitle,
|
||||
// **Das Hochkant-Plakat, nicht `thumb`.** Bei
|
||||
// einer Folge ist `thumb` das Szenenbild; im
|
||||
// Regal stünde davon ein zufälliger
|
||||
// Ausschnitt. Dieselbe Rückfallkette wie auf
|
||||
// den Kacheln.
|
||||
imageURL: backend.artworkURL(
|
||||
path: item.posterArtworkPath,
|
||||
width: posterWidth,
|
||||
height: posterHeight
|
||||
),
|
||||
progress: item.watchProgress
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.filter { !$0.entries.isEmpty }
|
||||
|
||||
TopShelfStore.write(
|
||||
TopShelfSnapshot(sections: Array(abschnitte)),
|
||||
group: group
|
||||
)
|
||||
|
||||
// **Und dem System sagen, dass es neu fragen soll.**
|
||||
//
|
||||
// Ohne diese Zeile bleibt die Erweiterung ungefragt, und im Header
|
||||
// steht, was dann geschieht: „If the completion handler is not called
|
||||
// in a timely manner or is called with empty content the system will
|
||||
// display the static image instead." Genau das war zu sehen — das
|
||||
// Plakat aus dem Asset-Katalog, gross und unverändert, während die
|
||||
// Datei daneben längst drei Reihen enthielt.
|
||||
//
|
||||
// Die Erweiterung selbst kann das nicht auslösen: sie läuft nur,
|
||||
// wenn sie gefragt wird.
|
||||
TVTopShelfContentProvider.topShelfContentDidChange()
|
||||
}
|
||||
|
||||
/// Was zuletzt abgelegt wurde — in einem Satz, zum Hinsehen.
|
||||
///
|
||||
/// **Weil man dem Regal sonst nichts ansieht.** Bleibt es leer, gibt es
|
||||
/// genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da,
|
||||
/// oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen
|
||||
/// alle drei gleich aus — nämlich nach dem grossen Plakat aus dem
|
||||
/// Asset-Katalog. Diese Zeile unterscheidet sie.
|
||||
static var status: String {
|
||||
guard let ort = TopShelfStore.location(group: group) else {
|
||||
return String(localized: "topShelf.noGroup")
|
||||
}
|
||||
guard let abzug = TopShelfStore.read(group: group) else {
|
||||
let daIst = FileManager.default.fileExists(atPath: ort.path)
|
||||
return daIst
|
||||
? String(localized: "topShelf.unreadable")
|
||||
: String(localized: "topShelf.noFile")
|
||||
}
|
||||
let einträge = abzug.sections.reduce(0) { $0 + $1.entries.count }
|
||||
let zeit = abzug.savedAt.formatted(date: .omitted, time: .shortened)
|
||||
return String(localized: "topShelf.written \(abzug.sections.count) \(einträge) \(zeit)")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user