Files
Flaro/Packages/HaloKit/Sources/HaloDesign/HaloFont.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 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>
2026-10-03 11:12:32 +02:00

186 lines
8.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}