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>
167 lines
7.9 KiB
Swift
167 lines
7.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 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
|
||
}
|