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>
98 lines
3.3 KiB
Swift
98 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
/// Wie Untertitel aussehen sollen.
|
|
///
|
|
/// **Warum hier und nicht im App-Ziel.** Weil es prüfbar sein muss: die
|
|
/// Übersetzung nach AVFoundation ist eine Tabelle aus Schlüsseln, bei der ein
|
|
/// Tippfehler lautlos dazu führt, dass **gar nichts** gesetzt wird. Ein Test
|
|
/// findet das, ein Blick auf den Bildschirm nicht — dort sieht „keine Regel"
|
|
/// genauso aus wie „Regel ohne Wirkung".
|
|
public struct SubtitleStyle: Sendable, Equatable {
|
|
/// Eine Farbe, wie CoreMedia sie versteht: vier Anteile von 0 bis 1.
|
|
public struct RGBA: Sendable, Equatable {
|
|
public let red: Double
|
|
public let green: Double
|
|
public let blue: Double
|
|
public let alpha: Double
|
|
|
|
public init(red: Double, green: Double, blue: Double, alpha: Double = 1) {
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
self.alpha = alpha
|
|
}
|
|
|
|
static let white = RGBA(red: 1, green: 1, blue: 1)
|
|
static let black = RGBA(red: 0, green: 0, blue: 0)
|
|
static let yellow = RGBA(red: 1, green: 0.85, blue: 0.2)
|
|
static let clear = RGBA(red: 0, green: 0, blue: 0, alpha: 0)
|
|
/// Nicht ganz deckend: ein voll schwarzer Kasten wirkt wie ein Loch
|
|
/// im Bild.
|
|
static let box = RGBA(red: 0, green: 0, blue: 0, alpha: 0.75)
|
|
}
|
|
|
|
/// Die Grösse, **relativ** zur Vorgabe des Players.
|
|
///
|
|
/// Ein Prozentwert, keine Punktgrösse: hundert heisst unverändert. So
|
|
/// bleibt die Einstellung richtig, egal wie gross das Bild ist.
|
|
public enum Size: Int, CaseIterable, Identifiable, Sendable, Codable {
|
|
case small = 75
|
|
case normal = 100
|
|
case large = 130
|
|
case huge = 175
|
|
|
|
public var id: Int { rawValue }
|
|
}
|
|
|
|
/// Schrift und Kasten als **Paar**, nicht als zwei Regler.
|
|
///
|
|
/// Unabhängige Achsen liessen unlesbare Mischungen zu — Schwarz auf
|
|
/// durchsichtig über einer Nachtszene ist eine gültige Einstellung und
|
|
/// eine wertlose. Diese fünf decken ab, was man tatsächlich will.
|
|
public enum Palette: String, CaseIterable, Identifiable, Sendable, Codable {
|
|
case whiteOnClear
|
|
case whiteOnBlack
|
|
case blackOnWhite
|
|
case yellowOnClear
|
|
case yellowOnBlack
|
|
|
|
public var id: String { rawValue }
|
|
}
|
|
|
|
public var size: Size
|
|
public var palette: Palette
|
|
|
|
public init(size: Size = .normal, palette: Palette = .whiteOnClear) {
|
|
self.size = size
|
|
self.palette = palette
|
|
}
|
|
|
|
public var foreground: RGBA {
|
|
switch palette {
|
|
case .whiteOnClear, .whiteOnBlack: .white
|
|
case .blackOnWhite: .black
|
|
case .yellowOnClear, .yellowOnBlack: .yellow
|
|
}
|
|
}
|
|
|
|
public var background: RGBA {
|
|
switch palette {
|
|
case .whiteOnClear, .yellowOnClear: .clear
|
|
case .whiteOnBlack, .yellowOnBlack: .box
|
|
case .blackOnWhite: RGBA(red: 1, green: 1, blue: 1, alpha: 0.85)
|
|
}
|
|
}
|
|
|
|
/// Ob ein Schlagschatten nötig ist.
|
|
///
|
|
/// Ohne Kasten steht die Schrift unmittelbar im Bild, und Weiss auf einer
|
|
/// hellen Szene ist dort unlesbar. Mit Kasten wäre der Schatten nur
|
|
/// Unruhe.
|
|
public var wantsEdgeShadow: Bool {
|
|
switch palette {
|
|
case .whiteOnClear, .yellowOnClear: true
|
|
case .whiteOnBlack, .blackOnWhite, .yellowOnBlack: false
|
|
}
|
|
}
|
|
}
|