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>
206 lines
7.5 KiB
Swift
206 lines
7.5 KiB
Swift
import SwiftUI
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#else
|
|
import AppKit
|
|
#endif
|
|
|
|
/// Was auf iPhone, Mac und Fernseher verschieden heißt — an einer Stelle.
|
|
///
|
|
/// Nicht jede Ansicht soll `#if os(iOS)` tragen. Ein Formular, das auf beiden
|
|
/// Plattformen dasselbe meint, soll auf beiden gleich aussehen; wo die
|
|
/// Systeme verschiedene Namen dafür haben, steht der Unterschied hier und
|
|
/// nirgends sonst. Sonst wandert er in zwanzig Dateien und wird beim
|
|
/// nächsten Bildschirm vergessen.
|
|
/// Auf was für einem Gerät läuft das hier gerade?
|
|
public enum HaloPlatform {
|
|
/// Läuft die App auf einem Mac?
|
|
///
|
|
/// **Zwei Wege führen dorthin, und für den Übersetzer sehen sie
|
|
/// verschieden aus.** Catalyst ist ein eigener Bau und meldet sich als
|
|
/// `targetEnvironment(macCatalyst)`. „Für iPad angepasst" ist dagegen
|
|
/// buchstäblich der iOS-Bau, der auf Apple Silicon startet — dort ist
|
|
/// dieselbe Abfrage schlicht falsch.
|
|
///
|
|
/// Wer nur die erste Hälfte prüft, schreibt Code für den Mac, der auf dem
|
|
/// Mac nicht läuft: übersetzt sauber, ist aber genau dort abgeschaltet,
|
|
/// wo er gebraucht wird. Deshalb hier beide Wege, an einer Stelle.
|
|
///
|
|
/// **Und warum unten `#error` steht.** Der `#else`-Zweig lautete `true` —
|
|
/// eine Antwort auf eine Frage, die niemand gestellt hatte. Auf tvOS fiel
|
|
/// er deshalb hindurch und meldete einen Mac: kein Übersetzungsfehler,
|
|
/// keine Warnung, nur ein Fernseher, der sich für einen Mac hielt. Eine
|
|
/// Vorgabe ist an dieser Stelle immer falsch, weil die nächste Plattform
|
|
/// nicht danach fragt, ob sie gemeint war. Jetzt bricht der Bau ab und
|
|
/// verlangt eine Entscheidung.
|
|
public static let isMac: Bool = {
|
|
#if os(tvOS)
|
|
false
|
|
#elseif targetEnvironment(macCatalyst)
|
|
true
|
|
#elseif os(iOS)
|
|
ProcessInfo.processInfo.isiOSAppOnMac
|
|
#elseif os(macOS)
|
|
true
|
|
#else
|
|
#error("HaloPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
|
|
#endif
|
|
}()
|
|
|
|
/// Läuft die App auf einem Fernseher?
|
|
///
|
|
/// Daneben und nicht in einem Aufzählungstyp: über die Frage wird
|
|
/// nirgends verzweigt, sie steht überall in einem Fragezeichen-Ausdruck.
|
|
/// Ein Aufzählungstyp wäre Zeremonie — und „iPhone oder iPad" liesse sich
|
|
/// darin ohnehin nicht beantworten, weil das `UIDevice.current` verlangt
|
|
/// und damit den Hauptakteur.
|
|
public static let isTV: Bool = {
|
|
#if os(tvOS)
|
|
true
|
|
#elseif os(iOS) || os(macOS)
|
|
false
|
|
#else
|
|
#error("HaloPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
|
|
#endif
|
|
}()
|
|
}
|
|
|
|
extension View {
|
|
/// Eine schmale Titelzeile — auf dem Mac und auf dem Fernseher gibt es
|
|
/// sie nicht. Beide fallen in denselben `#else`-Zweig, und beide zu Recht.
|
|
public func haloInlineTitle() -> some View {
|
|
#if os(iOS)
|
|
navigationBarTitleDisplayMode(.inline)
|
|
#else
|
|
self
|
|
#endif
|
|
}
|
|
|
|
/// Ein Sheet, das die halbe Höhe einnimmt.
|
|
///
|
|
/// Auf dem Mac ist ein Sheet ein Fenster: dort zählt keine Rastung,
|
|
/// sondern eine vernünftige Mindestgröße.
|
|
public func haloMediumSheet() -> some View {
|
|
#if os(tvOS)
|
|
// Auf dem Fernseher ist ein Sheet immer bildfüllend. `presentationDetents`
|
|
// übersetzt dort zwar — es gibt sie seit tvOS 16 —, bewirkt aber
|
|
// nichts. Und eine Zeile, die nichts bewirkt, liest sich beim nächsten
|
|
// Mal wie eine, die etwas bewirkt.
|
|
self
|
|
#else
|
|
// **Zur Laufzeit, nicht beim Übersetzen.** Der `#else`-Zweig für
|
|
// macOS griff nie: auf dem Mac läuft der iOS-Bau, und damit bekam ein
|
|
// grosses Fenster eine Sheet-Kante in iPhone-Höhe — ein schmaler
|
|
// Streifen, der unten im Nichts klebt.
|
|
presentationDetents(HaloPlatform.isMac ? [.large] : [.medium])
|
|
#endif
|
|
}
|
|
|
|
/// Dasselbe, aber auf Wunsch bis zur vollen Höhe aufziehbar.
|
|
public func haloResizableSheet() -> some View {
|
|
#if os(tvOS)
|
|
self
|
|
#else
|
|
// Siehe oben. Auf dem Mac gibt es kein „halb aufgezogen": ein Fenster
|
|
// hat die Höhe, die es hat, und eine Liste soll sie nutzen.
|
|
presentationDetents(HaloPlatform.isMac ? [.large] : [.medium, .large])
|
|
#endif
|
|
}
|
|
|
|
/// Ein Formular, das auf beiden Seiten gleich aussieht.
|
|
///
|
|
/// Auf tvOS gibt es `formStyle` nicht und es ist auch nicht nötig — dort
|
|
/// fällt es in denselben Zweig wie iOS.
|
|
///
|
|
/// Auf dem Mac setzt SwiftUI sonst den Spalten-Stil: Beschriftungen rechts
|
|
/// ausgerichtet, keine abgesetzten Abschnitte, alles eng. Der gruppierte
|
|
/// Stil ist derselbe, den iOS ohnehin nimmt — und der Grund, warum die
|
|
/// Einstellungen auf dem Mac wie ein Systemdialog aus einem anderen
|
|
/// Jahrzehnt aussahen.
|
|
public func haloForm() -> some View {
|
|
#if os(macOS)
|
|
formStyle(.grouped)
|
|
#else
|
|
self
|
|
#endif
|
|
}
|
|
|
|
/// Eingabefelder für Adressen und Namen: keine Großschreibung, keine
|
|
/// Korrektur — auf dem Mac gibt es beides ohnehin nicht als Tastatur.
|
|
///
|
|
/// Auf tvOS ebenfalls der `#else`-Zweig: die Bildschirmtastatur dort
|
|
/// kennt weder `keyboardType` noch Autokorrektur in dieser Form.
|
|
public func haloPlainEntry(isAddress: Bool = false) -> some View {
|
|
#if os(iOS)
|
|
self
|
|
.keyboardType(isAddress ? .URL : .default)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
#else
|
|
autocorrectionDisabled()
|
|
#endif
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Keine eigene Fläche hinter der Titelzeile.
|
|
///
|
|
/// Auf der Startseite liegt darunter der Aufmacher — eine weichgezeichnete
|
|
/// Leiste verschleierte die Hälfte davon. Auf dem Mac und auf dem
|
|
/// Fernseher gibt es die Leiste in dieser Form nicht — beide fallen in
|
|
/// den `#else`-Zweig.
|
|
public func haloClearToolbarBackground() -> some View {
|
|
#if os(iOS)
|
|
self
|
|
.toolbarBackground(.hidden, for: .navigationBar)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
#else
|
|
self
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// Etwas in die Zwischenablage legen.
|
|
///
|
|
/// UIKit und AppKit heißen verschieden und meinen dasselbe; die Diagnose soll
|
|
/// deswegen nicht mit `#if` beginnen.
|
|
public enum HaloClipboard {
|
|
public static func copy(_ text: String) {
|
|
#if os(tvOS)
|
|
// Es gibt keine. `UIPasteboard` ist auf tvOS als nicht verfügbar
|
|
// markiert — und ein Fernseher hat auch nichts, wohin man etwas
|
|
// einfügen könnte.
|
|
//
|
|
// **Die Reihenfolge ist der ganze Punkt.** `canImport(UIKit)` ist auf
|
|
// tvOS wahr; stünde dieser Zweig darunter, wäre es kein stiller
|
|
// Fehler, sondern ein lauter.
|
|
_ = text
|
|
#elseif canImport(UIKit)
|
|
UIPasteboard.general.string = text
|
|
#else
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(text, forType: .string)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
extension ToolbarItemPlacement {
|
|
/// Rechts in der Titelzeile.
|
|
public static var haloTrailing: ToolbarItemPlacement {
|
|
#if os(iOS) || os(tvOS)
|
|
.topBarTrailing
|
|
#else
|
|
.primaryAction
|
|
#endif
|
|
}
|
|
|
|
/// Links in der Titelzeile.
|
|
public static var haloLeading: ToolbarItemPlacement {
|
|
#if os(iOS) || os(tvOS)
|
|
.topBarLeading
|
|
#else
|
|
.navigation
|
|
#endif
|
|
}
|
|
}
|