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,165 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import MediaKit
|
||||
|
||||
/// Was abgespielt werden soll.
|
||||
struct PlaybackRequest: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let ratingKey: String
|
||||
/// Für die Titelzeile, solange die Metadaten noch geladen werden.
|
||||
let title: String
|
||||
/// Die zweite Zeile, wenn sie schon feststeht.
|
||||
///
|
||||
/// Bei Live-TV die laufende Sendung: sie steht nirgends nachzuschlagen —
|
||||
/// ein Sender hat keine Metadaten in der Mediathek —, und ohne sie sieht
|
||||
/// man im Player nur den Sendernamen und weiß nicht, was läuft.
|
||||
var subtitle: String?
|
||||
/// Wann die laufende Sendung endet.
|
||||
///
|
||||
/// Damit im Player nicht den ganzen Abend derselbe Name steht: ein Sender
|
||||
/// läuft weiter, die Sendung wechselt. Ohne diese Zeit wüsste niemand,
|
||||
/// wann die Angabe falsch wird.
|
||||
var subtitleEndsAt: Date?
|
||||
/// Welche Fassung, wenn ein Titel in mehreren vorliegt.
|
||||
let mediaIndex: Int
|
||||
/// Ab wo — aus dem gemeldeten Fortschritt.
|
||||
let startTime: Duration
|
||||
/// Eine bereits feststehende Adresse.
|
||||
///
|
||||
/// Für Live-TV. Ein Sender liegt nicht in der Mediathek, und der übliche
|
||||
/// Weg — Kennung nachschlagen, Datei suchen — findet für ihn nichts.
|
||||
/// Daran ist das Einschalten gescheitert: die Adresse war längst
|
||||
/// berechnet und wurde weggeworfen.
|
||||
var streamURL: URL?
|
||||
/// Womit der Tuner nach dem Zusehen wieder freigegeben wird.
|
||||
///
|
||||
/// Bei vier Tunern ist der fünfte Sender sonst nicht mehr einzuschalten.
|
||||
var liveSubscriptionKey: String?
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id }
|
||||
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
||||
}
|
||||
|
||||
/// Nimmt Abspielwünsche von überall entgegen.
|
||||
///
|
||||
/// Ohne diese Stelle bräuchte jeder Bildschirm, von dem aus sich etwas starten
|
||||
/// lässt — Aufmacher, Reihe, Detailseite, Suche — seinen eigenen
|
||||
/// Vollbild-Übergang. Vier Kopien desselben Ablaufs, die auseinanderdriften.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class PlaybackCoordinator {
|
||||
/// Wie der Player gerade zu sehen ist.
|
||||
///
|
||||
/// Der Unterschied zwischen „klein" und „gar nicht" ist der Kern der
|
||||
/// Sache: verkleinern darf die Wiedergabe **nicht** anhalten. Vorher gab
|
||||
/// es diese Unterscheidung nicht — der Player war da oder weg, und mit
|
||||
/// ihm die Engine, der Transcoder und beim Live-TV der belegte Tuner.
|
||||
enum Presentation: Equatable {
|
||||
case hidden
|
||||
case mini
|
||||
case full
|
||||
}
|
||||
|
||||
private(set) var request: PlaybackRequest?
|
||||
private(set) var presentation: Presentation = .hidden
|
||||
|
||||
func play(_ item: MediaItem, mediaIndex: Int = 0, resuming: Bool = true) {
|
||||
play(PlaybackRequest(
|
||||
ratingKey: item.id,
|
||||
title: item.playbackTitle,
|
||||
mediaIndex: mediaIndex,
|
||||
startTime: resuming ? item.resumeTime : .zero
|
||||
))
|
||||
}
|
||||
|
||||
/// Schaltet einen laufenden Sender ein.
|
||||
///
|
||||
/// Eigener Weg, weil ein Sender nicht in der Mediathek liegt: Adresse und
|
||||
/// Tuner-Belegung stehen schon fest und dürfen nicht neu ermittelt werden.
|
||||
func playLive(
|
||||
channelID: String,
|
||||
title: String,
|
||||
programme: String?,
|
||||
programmeEndsAt: Date?,
|
||||
stream: URL,
|
||||
subscriptionKey: String?
|
||||
) {
|
||||
play(PlaybackRequest(
|
||||
ratingKey: channelID,
|
||||
title: title,
|
||||
subtitle: programme,
|
||||
subtitleEndsAt: programmeEndsAt,
|
||||
mediaIndex: 0,
|
||||
startTime: .zero,
|
||||
streamURL: stream,
|
||||
liveSubscriptionKey: subscriptionKey
|
||||
))
|
||||
}
|
||||
|
||||
/// Übernimmt einen neuen Titel.
|
||||
///
|
||||
/// Läuft bereits die Kachel, bleibt es dabei: wer nebenher stöbert, will
|
||||
/// beim Antippen nicht ins Vollbild gerissen werden. Sonst Vollbild, wie
|
||||
/// man es von einem Antippen erwartet.
|
||||
func play(_ next: PlaybackRequest) {
|
||||
presentation = presentation == .mini ? .mini : .full
|
||||
request = next
|
||||
}
|
||||
|
||||
/// Verkleinert den Player zur schwebenden Kachel. Die Wiedergabe läuft.
|
||||
func minimize() {
|
||||
guard request != nil else { return }
|
||||
presentation = .mini
|
||||
}
|
||||
|
||||
func expand() {
|
||||
guard request != nil else { return }
|
||||
presentation = .full
|
||||
}
|
||||
|
||||
/// Beendet die Wiedergabe wirklich.
|
||||
///
|
||||
/// Die einzige Stelle, an der das passiert. Alles andere — verkleinern,
|
||||
/// vergrößern, Bild-in-Bild — lässt die Engine am Leben.
|
||||
func close() {
|
||||
request = nil
|
||||
presentation = .hidden
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaItem {
|
||||
/// Wie der Titel im Player heißt.
|
||||
///
|
||||
/// Bei Folgen ist der eigene Titel allein nutzlos — „Die erste Folge"
|
||||
/// sagt nichts darüber, welche Serie läuft.
|
||||
var playbackTitle: String {
|
||||
if kind == .episode, let series = seriesTitle {
|
||||
return "\(series) · \(title)"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
/// Die Beschriftung unter einer Kachel.
|
||||
///
|
||||
/// Sie ordnet ein, was darüber steht: zu einer Folge gehört die Serie, zu
|
||||
/// einer Staffel ihre Nummer. Die Reihenfolge ist Absicht — wird der
|
||||
/// Platz eng, bleibt der Serienname stehen und die Folgennummer fällt weg.
|
||||
var cardSubtitle: String? {
|
||||
switch kind {
|
||||
case .episode:
|
||||
var code: String?
|
||||
if let season = parentIndex, let episode = index {
|
||||
code = String(localized: "episode.short \(season) \(episode)")
|
||||
}
|
||||
guard let series = seriesTitle else { return code }
|
||||
return code.map { "\(series) · \($0)" } ?? series
|
||||
case .season:
|
||||
// Ohne die Serie darüber wäre „Staffel 10" beliebig — deshalb
|
||||
// steht die Nummer hier und der Serienname als Titel.
|
||||
if let index { return String(localized: "season.short \(index)") }
|
||||
return parentTitle == nil ? nil : title
|
||||
default:
|
||||
return year.map(String.init)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user