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>
186 lines
8.9 KiB
Swift
186 lines
8.9 KiB
Swift
import SwiftUI
|
||
|
||
/// Die Schriftskala der App.
|
||
///
|
||
/// Auf iOS die Systemstile, damit Dynamic Type wirkt. Die Rolle steht im
|
||
/// Namen, nicht die Größe — sonst führt jede Anpassung zu einer Suche quer
|
||
/// durch alle Views.
|
||
///
|
||
/// **Auf dem Mac stehen feste Größen.** Dieselben Stile sind dort deutlich
|
||
/// kleiner: `largeTitle` misst 26 Punkt statt 34, `body` 13 statt 17. Eine
|
||
/// Oberfläche, die auf dem iPad stimmt, wirkt damit auf dem Mac zusammen-
|
||
/// geschrumpft — und genau das war sie. Die Zahlen hier sind die von iOS.
|
||
///
|
||
/// Das kostet auf dem Mac Dynamic Type. Das ist vertretbar: macOS hat keine
|
||
/// systemweite Textgrößen-Einstellung, auf die Apps reagieren müssten, und ein
|
||
/// Fenster lässt sich stattdessen größer ziehen.
|
||
///
|
||
/// **Auf dem Fernseher wieder Systemstile, aber andere Rollen.** Dynamic Type
|
||
/// gibt es dort auch, und dass `body` grösser ausfällt als am Telefon, ist
|
||
/// der Sinn der Sache — drei Meter Abstand. Falsch wäre nur, die iOS-Zuordnung
|
||
/// einfach durchfallen zu lassen: die Skala ist dort anders gespreizt, und
|
||
/// eine Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern
|
||
/// Zierrat.
|
||
extension Font {
|
||
#if os(macOS)
|
||
/// Der Titel im Aufmacher-Bild.
|
||
public static let haloHero = Font.system(size: 34, weight: .bold)
|
||
/// Titel auf Detailseiten.
|
||
public static let haloTitle = Font.system(size: 22, weight: .semibold)
|
||
/// Überschrift einer Reihe auf der Startseite.
|
||
public static let haloRowHeader = Font.system(size: 17, weight: .semibold)
|
||
/// Titel unter einem Poster.
|
||
public static let haloCardTitle = Font.system(size: 15, weight: .medium)
|
||
/// Fließtext, Beschreibungen.
|
||
public static let haloBody = Font.system(size: 17)
|
||
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
||
public static let haloCaption = Font.system(size: 12)
|
||
#elseif os(tvOS)
|
||
/// Der Titel im Aufmacher-Bild.
|
||
///
|
||
/// **`.title` und nicht `.largeTitle`.** In SwiftUI gibt es den Stil auf
|
||
/// tvOS, in UIKit nicht — `UIFontTextStyleLargeTitle` ist dort als nicht
|
||
/// verfügbar markiert. Was SwiftUI daraus macht, steht nirgends, und eine
|
||
/// Schrift, deren Grösse niemand nennen kann, gehört nicht in die grösste
|
||
/// Zeile der App.
|
||
public static let haloHero = Font.system(.title, design: .default, weight: .bold)
|
||
/// Titel auf Detailseiten.
|
||
public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold)
|
||
/// Überschrift einer Reihe auf der Startseite.
|
||
public static let haloRowHeader = Font.system(.headline, design: .default, weight: .semibold)
|
||
/// Titel unter einem Poster — auf tvOS nur bei Fokus sichtbar.
|
||
///
|
||
/// Eine Stufe tiefer als auf iOS, und das ist gewollt: auf tvOS ist
|
||
/// `caption` schon gross, und ein Raster aus sechs Spalten mit
|
||
/// `subheadline`-Titeln hätte mehr Schrift als Bild.
|
||
public static let haloCardTitle = Font.system(.caption, design: .default, weight: .medium)
|
||
/// Fließtext, Beschreibungen.
|
||
public static let haloBody = Font.system(.body)
|
||
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
||
public static let haloCaption = Font.system(.caption2)
|
||
#else
|
||
/// Der Titel im Aufmacher-Bild.
|
||
public static let haloHero = Font.system(.largeTitle, design: .default, weight: .bold)
|
||
/// Titel auf Detailseiten.
|
||
public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold)
|
||
/// Überschrift einer Reihe auf der Startseite.
|
||
public static let haloRowHeader = Font.system(.headline, design: .default, weight: .semibold)
|
||
/// Titel unter einem Poster.
|
||
public static let haloCardTitle = Font.system(.subheadline, design: .default, weight: .medium)
|
||
/// Fließtext, Beschreibungen.
|
||
public static let haloBody = Font.system(.body)
|
||
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
||
public static let haloCaption = Font.system(.caption)
|
||
#endif
|
||
}
|
||
|
||
/// Abstände.
|
||
///
|
||
/// Eine Skala statt frei gewählter Zahlen: dadurch stimmen die Ränder zwischen
|
||
/// Bildschirmen überein, ohne dass man sie einzeln nachmisst.
|
||
public enum HaloSpacing {
|
||
#if os(tvOS)
|
||
// Durchweg doppelt: ein Fernseher ist 1920 Punkte breit und wird aus drei
|
||
// Metern gelesen. Acht Punkte Abstand sind dort keine Trennung mehr.
|
||
public static let tight: CGFloat = 8
|
||
public static let snug: CGFloat = 16
|
||
public static let regular: CGFloat = 24
|
||
public static let loose: CGFloat = 32
|
||
public static let section: CGFloat = 48
|
||
/// Der seitliche Rand des Inhalts — **vom Bildschirmrand aus**.
|
||
///
|
||
/// Achtzig Punkte Überscan plus achtundvierzig Punkte Rinne.
|
||
///
|
||
/// **Warum die achtzig hier drinstehen.** SwiftUI legt sie von selbst als
|
||
/// Sicherheitsrand an, und normalerweise nähme man sie geschenkt. Der
|
||
/// Aufmacher muss aber bis an die Bildschirmkante reichen — ein Bild, das
|
||
/// achtzig Punkte vor dem Rand aufhört, sieht auf einem Fernseher nach
|
||
/// Fehler aus. Also laufen die Seiten dieser App bildfüllend
|
||
/// (`ignoresSafeArea`), und dann muss der Rand hier wieder dazu.
|
||
///
|
||
/// Das heisst zugleich: wer diesen Wert benutzt, ignoriert den
|
||
/// Sicherheitsrand. Beides gehört zusammen.
|
||
public static let screenMargin: CGFloat = 128
|
||
#else
|
||
public static let tight: CGFloat = 4
|
||
public static let snug: CGFloat = 8
|
||
public static let regular: CGFloat = 12
|
||
public static let loose: CGFloat = 16
|
||
public static let section: CGFloat = 24
|
||
/// Der seitliche Rand des Inhalts zum Bildschirmrand.
|
||
public static let screenMargin: CGFloat = 16
|
||
#endif
|
||
}
|
||
|
||
/// Der Überscan-Bereich eines Fernsehers.
|
||
///
|
||
/// **Was das ist.** Ein Fernseher zeigt nicht zwingend das ganze Bild: je nach
|
||
/// Gerät und Einstellung schneidet er am Rand ab, und was dort steht, sieht
|
||
/// niemand.
|
||
///
|
||
/// **Nachgemessen, nicht abgeschrieben.** Im Simulator eines Apple TV 4K
|
||
/// meldet SwiftUI bei 1920×1080 genau diese Ränder — sechzig oben und unten,
|
||
/// **achtzig** seitlich, Nutzfläche also 1760×960. Die verbreitete Angabe
|
||
/// „neunzig" stimmt nicht; eine zweite Quelle nannte achtzig und hatte recht.
|
||
///
|
||
/// **Und man bekommt sie geschenkt.** SwiftUI legt sie von selbst als
|
||
/// Sicherheitsrand an — deshalb steht in `HaloSpacing.screenMargin` nur die
|
||
/// Rinne innerhalb davon. Die Zahl hier ist für die Gegenrichtung: ein
|
||
/// bildfüllender Aufmacher, der den Sicherheitsrand absichtlich übergeht und
|
||
/// dessen Schrift trotzdem drinbleiben muss.
|
||
public enum HaloSafeArea {
|
||
public static let tv = EdgeInsets(top: 60, leading: 80, bottom: 60, trailing: 80)
|
||
}
|
||
|
||
public enum HaloRadius {
|
||
/// Die Ecke einer Kachel.
|
||
///
|
||
/// Auf dem Fernseher grösser: eine Kachel, die sich beim Fokus anhebt,
|
||
/// wirkt mit acht Punkten bei 1920 Punkten Breite kantig.
|
||
#if os(tvOS)
|
||
public static let card: CGFloat = 12
|
||
#else
|
||
public static let card: CGFloat = 8
|
||
#endif
|
||
public static let sheet: CGFloat = 16
|
||
public static let control: CGFloat = 10
|
||
/// Eine schwebende Fläche — die Seitenleiste. So rund wie das System sie
|
||
/// auf dem iPad zeichnet; daran misst sich der Nachbau.
|
||
public static let panel: CGFloat = 22
|
||
}
|
||
|
||
/// Die Seitenverhältnisse, in denen Plex seine Bilder liefert.
|
||
///
|
||
/// An einer Stelle, weil ein falscher Wert nicht auffällt, sondern nur
|
||
/// beschneidet: ein 16:9-Hintergrundbild in einem 3:4-Rahmen verliert links
|
||
/// und rechts je ein Viertel, und man sieht dem Ergebnis nicht an, dass etwas
|
||
/// fehlt.
|
||
public enum HaloAspectRatio {
|
||
/// Kinoplakate im Hochformat.
|
||
public static let poster: CGFloat = 2.0 / 3.0
|
||
/// Szenenbilder und Hintergrundbilder.
|
||
public static let landscape: CGFloat = 16.0 / 9.0
|
||
/// Der Aufmacher auf der Startseite — dasselbe wie `landscape`, weil Plex
|
||
/// dafür `art` liefert.
|
||
public static let hero: CGFloat = landscape
|
||
/// Der Aufmacher auf einem schmalen Bildschirm.
|
||
///
|
||
/// **Quer, aber beschnitten — und beides hat einen Grund.**
|
||
///
|
||
/// Quer, weil dort ein Hintergrundbild liegt. Vorher lag hier ein Plakat
|
||
/// (2:3), und das ist für einen Aufmacher das falsche Bild: ein Plakat
|
||
/// trägt den Titel schon selbst, und darüber liegt dann noch der
|
||
/// freigestellte Schriftzug — zweimal derselbe Name.
|
||
///
|
||
/// Beschnitten, weil 16:9 auf einem iPhone keine zweihundertfünfzig
|
||
/// Punkte Höhe sind, und darin sollen Schriftzug, Zeile mit Jahr und
|
||
/// Länge, ein Satz Beschreibung und zwei Knöpfe Platz finden. Sie fanden
|
||
/// ihn nicht und lagen übereinander. 4:3 gibt dem Bild rund ein Drittel
|
||
/// mehr Höhe; dafür fehlt ihm links und rechts zusammen ein Viertel.
|
||
public static let heroCompact: CGFloat = 4.0 / 3.0
|
||
|
||
/// Sendersymbole. Breiter als hoch, weil Senderlogos Schriftzüge sind —
|
||
/// quadratisch beschnitten fehlt der halbe Name.
|
||
public static let channelLogo: CGFloat = 5.0 / 3.0
|
||
}
|