Files
Flaro/Packages/FlixyKit/Sources/FlixyDesign/FlixyFont.swift
T
ScarriffleandClaude Opus 5 6010229b80 Die Pakete wissen jetzt, was ein Fernseher ist
Schritt eins zur Apple-TV-Fassung: nichts davon ist sichtbar, aber ohne
das lässt sich für tvOS nicht einmal übersetzen.

**Der stille Fehler zuerst.** `FlixyPlatform.isMac` hatte einen
`#else`-Zweig, der `true` zurückgab — eine Antwort auf eine Frage, die
niemand gestellt hatte. Auf tvOS fiel er dort hindurch: kein
Übersetzungsfehler, keine Warnung, nur ein Fernseher, der sich für einen
Mac hielt. Jetzt steht dort `#error`. Die nächste Plattform bricht den
Bau ab und verlangt eine Entscheidung, statt sich eine auszusuchen. Ein
Test hält fest, dass nicht beides zugleich gelten darf; er hätte genau
diesen Fehler gefangen.

**Ein echter Übersetzungsfehler**, und er lag an der Reihenfolge:
`UIPasteboard` steht hinter `canImport(UIKit)`, und das ist auf tvOS
wahr. Der tvOS-Zweig steht deshalb jetzt davor — ein Fernseher hat
ohnehin nichts, wohin man etwas einfügen könnte.

**Drei Vermutungen waren falsch**, nachgeschlagen im tvOS-SDK statt
geraten: `presentationDetents` gibt es dort seit tvOS 16 (übersetzt,
bewirkt nur nichts — jetzt steht es ausdrücklich da), `.topBarTrailing`
seit 14.0, und `isMac` hatte acht Fundstellen in fünf Dateien, nicht
fünfzig in siebenundzwanzig.

**Schrift und Abstände** bekommen einen eigenen Zweig statt durchzufallen.
Nicht feste Grössen: Dynamic Type gibt es auf tvOS auch, und dass `body`
dort grösser ausfällt, ist der Sinn der Sache. Falsch war nur die
Zuordnung der Rollen — die Skala ist anders gespreizt, und eine
Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern
Zierrat. Der Aufmacher geht auf `.title`, weil UIKit `largeTitle` auf
tvOS gar nicht kennt und nirgends steht, was SwiftUI daraus macht.
`screenMargin` steht auf 48 und nicht auf 90: die neunzig Punkte des
Überscan-Bereichs liefert das System als Sicherheitsrand, und wer sie
hier aufschlägt, hat hundertachtzig.

**Zwei Dinge, die sich erst beim Bauen zeigten.** `FlixyDownloads`
übersetzte nicht — `volumeAvailableCapacityForImportantUsage` gibt es
auf tvOS nicht. Das Paket wird dort trotzdem mitverlinkt, damit
`PlayerModel` den Koordinator nennen darf, ohne dass zweitausend Zeilen
Wiedergabelogik von `#if` durchzogen werden; angelegt wird nie einer.
Und `FlixyProbe` liess das ganze Paket scheitern: ein
Kommandozeilenwerkzeug mit `getpass` und `Host`, das auf einem Fernseher
nie laufen sollte, hielt den Bau für alle anderen auf. Es ist jetzt
ausdrücklich macOS-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:09:18 +02:00

167 lines
7.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 flixyHero = Font.system(size: 34, weight: .bold)
/// Titel auf Detailseiten.
public static let flixyTitle = Font.system(size: 22, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let flixyRowHeader = Font.system(size: 17, weight: .semibold)
/// Titel unter einem Poster.
public static let flixyCardTitle = Font.system(size: 15, weight: .medium)
/// Fließtext, Beschreibungen.
public static let flixyBody = Font.system(size: 17)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let flixyCaption = 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 flixyHero = Font.system(.title, design: .default, weight: .bold)
/// Titel auf Detailseiten.
public static let flixyTitle = Font.system(.title2, design: .default, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let flixyRowHeader = 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 flixyCardTitle = Font.system(.caption, design: .default, weight: .medium)
/// Fließtext, Beschreibungen.
public static let flixyBody = Font.system(.body)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let flixyCaption = Font.system(.caption2)
#else
/// Der Titel im Aufmacher-Bild.
public static let flixyHero = Font.system(.largeTitle, design: .default, weight: .bold)
/// Titel auf Detailseiten.
public static let flixyTitle = Font.system(.title2, design: .default, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let flixyRowHeader = Font.system(.headline, design: .default, weight: .semibold)
/// Titel unter einem Poster.
public static let flixyCardTitle = Font.system(.subheadline, design: .default, weight: .medium)
/// Fließtext, Beschreibungen.
public static let flixyBody = Font.system(.body)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let flixyCaption = 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 FlixySpacing {
#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 — **zusätzlich** zum Sicherheitsrand.
///
/// **Nicht 90.** Die neunzig Punkte des Überscan-Bereichs liefert das
/// System als Sicherheitsrand; wer sie hier noch einmal aufschlägt, hat
/// hundertachtzig und ein Raster, das in der Bildmitte klebt. Hier steht
/// nur die Rinne innerhalb des sicheren Bereichs.
public static let screenMargin: CGFloat = 48
#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. Apple nennt für 1920×1080 einen sicheren Bereich von neunzig
/// Punkten seitlich und sechzig oben wie unten.
///
/// Ein **Bild** darf darüber hinausgehen — ein Aufmacher soll das sogar.
/// Schrift und Knöpfe nicht. Die Zahl steht hier für die Fälle, in denen der
/// Sicherheitsrand absichtlich übergangen wird und der Text trotzdem drinnen
/// bleiben muss.
public enum FlixySafeArea {
public static let tv = EdgeInsets(top: 60, leading: 90, bottom: 60, trailing: 90)
}
public enum FlixyRadius {
/// 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 FlixyAspectRatio {
/// 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.
///
/// Nicht 16:9: auf einem iPhone sind das keine zweihundertzwanzig Punkte
/// Höhe, und darin sollen Titel oder Schriftzug, Zeile mit Jahr und Länge,
/// ein Satz Beschreibung und zwei Knöpfe Platz finden. Sie fanden ihn
/// nicht und lagen übereinander. Hochkant ist hier auch die ehrlichere
/// Form — ein Handy hält man so.
public static let heroCompact: CGFloat = 3.0 / 4.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
}