Deine Frage war die richtige. Eine native Mac-App sah an zwei Dutzend Stellen
anders aus, weil AppKit dieselben Dinge anders misst: Schriftgrößen,
Spaltenbreiten, Formularstil. Jede davon habe ich heute einzeln nachgezogen —
und wäre bei der nächsten Ansicht wieder von vorn angefangen.
Mac Catalyst nimmt die iPad-Oberfläche unverändert. Nicht nachgebaut, sondern
dieselbe: gleiche Schrift, gleiche Leiste, gleiche Abstände, und zwar auch in
allem, was noch kommt.
**Ich habe „MKV geht dann nie" gesagt. Das war falsch.** Ohne VLC spielen sie
nicht *unverändert* — der Server rechnet sie um, und das kann er längst. Nur
war die App darauf nicht vorbereitet: die Entscheidung hätte weiterhin VLC
gewählt, den es dort nicht gibt, und das Bild wäre schwarz geblieben.
`PlaybackSettings.hasFallbackEngine` fragt jetzt, ob es den zweiten Spieler auf
dieser Plattform überhaupt gibt. Wo nicht, übernimmt der Server. Das kostet ihn
Rechenzeit und sieht man nicht.
Unverändert abspielen kommt später zurück — dafür braucht es einen Decoder mit
einem Catalyst-Teil. VLCKits Binärpaket hat keinen:
ios-arm64 ios-simulator macos-arm64_x86_64 tvos watchos xros
Drei Stellen mussten Catalyst ausdrücklich ausnehmen: dort gilt `os(iOS)`, aber
ActivityKit gibt es nicht — die Typen sind da und jeder Aufruf ist als nicht
verfügbar markiert. Das ist die Art Falle, die erst der Compiler findet.
Die Anpassungen von vorhin bleiben stehen, obwohl Catalyst sie nicht braucht:
FlixyDesign baut für macOS mit, und `swift test` läuft dort. Sie kosten nichts
und halten den Weg zu einer nativen Mac-App offen, falls VLC ihn eines Tages
nötig macht.
263 Tests grün, iOS baut unverändert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die vier erfundenen Stufen weichen der Leiter, die der Server selbst
meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000,
12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine
eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt —
er rundet dann still auf die nächste, und die Anzeige log.
Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p
hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus.
Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene
Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten
Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere,
was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort.
„Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen
reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild
ausgeblendet.
Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die
Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt
sonst das doppelte.
Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder
rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im
Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören.
Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg,
er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben.
Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler.
Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live-TV
- An keinem echten Server geprüft: die DVR- und EPG-Endpunkte sind der am
schlechtesten dokumentierte Teil der Plex-API. Die Modelle sind deshalb
durchgehend nachsichtig — fehlende Felder ergeben leere Listen, nicht
Fehler, und die Senderkennung wird als Zahl, Zeichenkette oder uuid
akzeptiert.
- Ohne eingerichteten Empfänger erklärt die App das, statt einen Fehler
zu zeigen. Das ist der häufigste Fall.
- Senderliste mit laufender Sendung, Fortschrittsbalken und „danach".
Der Programmführer wird getrennt geholt: fällt er aus, bleibt das
Umschalten möglich.
- Die Anbieterkennung wird aus /media/providers gelesen statt fest
verdrahtet — sie wechselt je nach Server.
Live Activity
- Eigene Widget-Erweiterung; iOS zeichnet den Sperrbildschirm in einem
getrennten Prozess, während die App meist gar nicht läuft.
- Der Presenter drosselt auf 1,5 Sekunden. ActivityKit verwirft zu
häufige Aktualisierungen kommentarlos, und der Fortschritt kommt
mehrmals je Sekunde — ungebremst bliebe die Anzeige stehen.
- @preconcurrency import: Activity und ActivityContent tragen keine
Sendable-Zusage.
Zwei Stolpersteine unterwegs: canImport(ActivityKit) ist auf macOS wahr,
die Typen darin aber nicht verfügbar — es muss os(iOS) sein. Und eine
von Hand geschriebene Info.plist ohne CFBundleIdentifier führt zu einer
Erweiterung ganz ohne Kennung; der Fehler lautet dann „not prefixed with
the parent app's bundle identifier" und zeigt auf die falsche Stelle.
126 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Engine
- Hintergrund-URLSession: läuft weiter, wenn die App zu ist. Der ganze
Zustand liegt in SwiftData, weil es nach einem Neustart kein anderes
Gedächtnis gibt.
- Pausieren über cancel(byProducingResumeData:) — nur so lässt sich
weiterladen statt neu zu beginnen. Bei 40 GB der Unterschied
zwischen brauchbar und unbrauchbar.
- Zwei Übertragungen gleichzeitig, Folgen in Staffel-/Folgenreihenfolge.
Alles parallel zu laden hieße: keine Folge wird fertig.
- Speicherlimit und Geräteplatz werden vor dem Annehmen geprüft, ein
Gigabyte bleibt als Puffer frei.
- Verwaiste Dateien werden beim Start entfernt; sie entstehen, wenn die
App genau beim Abschluss beendet wird.
- Downloads liegen in Application Support, nicht in Caches (iOS räumt
den eigenmächtig leer) und sind von der iCloud-Sicherung ausgenommen.
Oberfläche
- Download-Knopf auf Detailseiten mit vier Zuständen in einem Knopf.
- „Alle Folgen laden" für Staffel und Serie; ein abgelehnter Titel
stoppt den Stapel nicht.
- Downloads-Ansicht mit Belegung, WLAN-Schalter, Pausieren, Fortsetzen,
Löschen — nach Serie gruppiert.
Offline
- Der Player bevorzugt die lokale Datei, auch ohne erreichbaren Server.
- Beim Herunterladen wird ein Abzug der Serverangaben mitgesichert:
offline lässt sich nichts nachfragen, und der PlaybackDecider braucht
Container und Codecs.
Ein Fehler, den die Tests aufgedeckt haben: der Koordinator hielt nur
den ModelContext, nicht den ModelContainer. Ein ModelContext hält seinen
Container nicht am Leben — wird der anderswo freigegeben, stürzt jeder
Zugriff auf einen Eintrag ab, und die Meldung zeigt auf ModelContext.reset
statt auf die Ursache. Jetzt liegt die Zuständigkeit beim Koordinator.
102 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>