Schritt 2 des Plans, erste Hälfte. Vite mit TypeScript, und ein Server
ohne eine einzige Abhängigkeit — er soll auf einem kleinen Container
laufen, nicht sein halbes Verzeichnis mitschleppen.
Der Server ist nicht Zierde. Er löst zwei Dinge, gegen die keine Zeile im
Frontend etwas ausrichten kann: Anfragen laufen über dieselbe Adresse wie
die Seite, womit CORS erledigt ist, auch beim Streamen. Und läuft der
Medienserver im Klartext, während die Seite verschlüsselt ausgeliefert
wird, blockiert der Browser sonst den Strom.
Geprüft: Weiterreichen mitsamt Autorisierung und richtig gesetztem Host,
Ausliefern gebauter Dateien, unbekannte Wege landen auf der Seite — und
ein Ausbruchsversuch mit "../../../etc/passwd" liefert die Seite statt
der Passwortdatei.
Der Körper wird durchgereicht statt gepuffert: ein Film ist ein Strom,
und ihn erst vollständig einzulesen hieße, ihn zwischenzuspeichern.
Die einzelne Datei bleibt vorerst, sie läuft ja.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sie hingen an einem verschluckten catch. Beim Suchen nach dem verlorenen
Fortschritt war deshalb nirgends zu sehen, ob sie überhaupt ankommen —
ohne await zu senden ist richtig, ohne Hinsehen nicht.
Jetzt steht in der Konsole, was angenommen und was abgelehnt wurde.
Dazu ein Hinweis auf die Regel des Servers, die den eigentlichen Fall
erklärt: Jellyfin merkt sich eine Fortsetzposition nur zwischen fünf und
neunzig Prozent der Laufzeit. Eine halbe Minute von einem Film liegt
darunter, und der Server verwirft sie bewusst — "kurz reingeschaut" ist
kein Weiterschauen. Wer das nicht weiß, sucht den Fehler bei sich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Schließen wurde das Video abgeräumt, und dabei fiel currentTime auf
null — bevor die Stopp-Meldung hinausging. Jellyfin bekam "gestoppt bei
0:00" und löschte den Fortschritt. Der Film war zur Hälfte gesehen und
stand trotzdem nicht unter "Weiterschauen".
Jetzt wird die Stelle getrennt vom Element gehalten und die Meldung
gesendet, bevor abgeräumt wird. Nachgemessen: bei 45 Sekunden
geschlossen, 45 Sekunden gemeldet. Dazu PlayMethod und CanSeek, ohne die
Jellyfin die Meldung keiner Art von Wiedergabe zuordnet, und sofortige
Meldung bei Anhalten und Weiterlaufen — wer pausiert und weggeht, hatte
sonst bis zur nächsten Zehn-Sekunden-Marke nichts hinterlassen.
Und das eingefrorene Bild bei laufendem Ton nach einem
Anwendungswechsel: Der Browser hält das Entpacken an, während der Tab
verdeckt ist, und hls.js findet danach nicht von selbst in den Puffer
zurück. Jetzt wird beim Zurückkommen an derselben Stelle neu angestoßen,
mit einem winzigen Sprung — ohne den bleibt das zuletzt gezeigte Bild
stehen, obwohl längst wieder Segmente ankommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ich habe nach dem Code gebaut und dabei das Wesentliche übersehen: In
PlayerView gibt es genau ein "background", und das ist die Abdunklung
über dem ganzen Bild. Die Knöpfe selbst sind blanke weiße Symbole.
Meine runden, hinterlegten Flächen waren eine Erfindung — und der Grund,
warum der Qualitätsknopf schief aussah: ein Wort passt in keinen Kreis.
Jetzt steht "Original" als das da, was es ist, nämlich Text.
Dazu die Spultasten als Kreispfeile mit der Sekundenzahl darin, wie
gobackward.10 und goforward.30 sie zeichnen, statt als "−10" und "+30"
in Textform. Und der Abspielpfeil in 52 Punkt, der Größe aus der App.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der strukturelle Unterschied, den ich übersehen hatte: in der iPad-App
liegt unten nichts als der Regler über die volle Breite, darunter Zeit
links und Restzeit rechts. Bei mir standen Zeit, Regler und Restzeit
nebeneinander — der Regler wirkte dadurch wie ein Formularfeld zwischen
zwei Beschriftungen — und darunter lag noch eine Werkzeugleiste mit
Lautstärkeregler und Tempo-Auswahlfeld, die es in der App gar nicht gibt.
Alles Einstellbare sitzt jetzt oben rechts beisammen: Tonspur,
Untertitel, Qualität, Geschwindigkeit, Ton, Vollbild. Das Tempo als
Liste wie die anderen; ein Auswahlfeld mitten in der Bedienleiste war das
einzige Bauteil, das nach Formular aussah. Die Lautstärke über Mausrad
auf dem Tonknopf und über die Pfeiltasten.
Und ein Fehler, der genau das verhinderte, was gewünscht war: Die
Bedienfläche lag als Ganzes über dem Bild und schluckte jeden Klick.
"Ins Bild tippen hält an" funktionierte damit nur, solange die Bedienung
unsichtbar war — also fast nie. Jetzt nehmen nur ihre Knöpfe Klicks
entgegen. Nachgemessen: Klick pausiert, Pfeil rechts spult 30 Sekunden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mein Menü machte genau den Fehler, den der iPad-Code im Kommentar
ausschließt: alles in einer Liste. Dort steht, warum das falsch ist —
"die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der anderen
Liste vorbei."
Jetzt drei Knöpfe in der Kopfzeile, jeder mit seiner eigenen Liste
darunter, und jede Liste hängt sichtbar an ihrem Knopf. Die Tonspur
erscheint auch dann, wenn es nur eine gibt — welche läuft, will man
wissen, nicht nur ob man wechseln kann. Die Untertitel-Taste ist
gefüllt, solange welche laufen.
Die Qualität steht als Text im Knopf, nicht als Symbol. Auch das aus dem
iPad-Player übernommen: "Drei Schieberegler sagen nicht, was eingestellt
ist; '1080p' schon — und genau das ist die Frage, wegen der man hintippt."
Dazu eine gleichmäßige Abdunklung statt zweier Verläufe an den Rändern.
Die Verläufe ließen die Mitte frei — wo die Transportknöpfe stehen, die
damit auf hellen Szenen nichts hinter sich hatten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sie standen bisher nur in den Einstellungen — also dort, wo man sie nicht
braucht. Man merkt beim Schauen, dass der Ton die falsche Sprache hat,
nicht vorher.
Jetzt ein Menü über der Bedienleiste, nicht als Blatt: man stellt hier
etwas ein, das man am Bild beurteilen will, und ein Blatt verdeckt genau
das. Die Auswahl kommt vom Server — welche Spuren es gibt, weiß man erst
nach dem Aushandeln, und seine Vorauswahl gilt beim ersten Lauf.
Anders als die App, die im Lauf umschaltet, muss der Browser neu
aushandeln: bei einem umgerechneten HLS-Strom bestimmt der Server beim
Aushandeln, was drin ist. Also wird neu geladen — und die Stelle
mitgenommen, sonst begänne jeder Sprachwechsel den Film von vorn.
Nachgemessen: bei 27:30 gewechselt, bei 27:30 weiter.
Dabei zwei Dinge mitgenommen, die sonst später weh getan hätten: der
Fehler-Horcher wird nur einmal angehängt (sonst meldete sich nach zwei
Wechseln ein einziger Fehler dreimal), und ein früherer HLS-Strom wird
abgeräumt, bevor der nächste anläuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Dinge.
Erstens: Symbole sind jetzt Vektoren, keine Schriftzeichen. Ein Lautsprecher
als Emoji wird auf jeder Plattform anders gezeichnet, trägt fremde Farben,
sitzt schief auf der Grundlinie und lässt sich weder in Strichstärke noch
Farbe anpassen — es sieht nach Platzhalter aus, weil es einer ist. Elf
Stellen umgestellt, die Pfade stehen an einer Stelle beisammen.
Zweitens, und das ist der Grund, warum nichts lief: Nennt der Server keine
eigene Adresse, fiel der Code auf "static=true" zurück — also auf die Datei,
wie sie auf der Platte liegt. Bei einem MKV mit DTS ist das für einen Browser
unlesbar, und er meldet bloß "Format nicht unterstützt". Der Rückfall tat
genau das Gegenteil dessen, was er sollte. Jetzt verlangt er master.m3u8,
also ausdrücklich einen HLS-Strom.
Damit so etwas nicht wieder im Dunkeln liegt, nennt die Fehleranzeige nun
auch, was der Server geantwortet hat und welche Adresse verwendet wurde —
der Zugangsschlüssel darin gekürzt.
Dazu: während des Ladens treten die Transportknöpfe zurück. Der Kreisel lag
vorher genau hinter dem größten von ihnen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei berechtigte Beschwerden auf einmal.
Das war kein Player, sondern das nackte <video controls> des Browsers in
einem Blatt — ein eingebettetes Vorschaufenster. Jetzt Vollbild mit
eigener Bedienung: Titel und Folge oben, Spulen und Anhalten in der
Mitte, Fortschritt mit Restzeit unten, dazu Lautstärke, Tempo und
Vollbild. Die Bedienung blendet nach drei Sekunden Ruhe aus und kommt
bei jeder Bewegung zurück; steht das Bild, bleibt sie stehen — sie
auszublenden, während niemand spielt, versteckt den einzigen Knopf, der
weiterhilft. Tastatur: Leertaste, Pfeile, Escape.
Dunkle Knöpfe statt hell durchscheinender: über einer sonnigen Szene
verschwanden helle vollständig. Und die Sekundenzahl steht als Text, statt
im Spulpfeil zu stecken — übereinander war beides unlesbar.
Und bei einer Serie "spielt man nicht ab" zu antworten war Unsinn. Ich
hatte den Knopf entfernt, statt ihn richtig zu machen. Jetzt heißt er
"Weiterschauen" und startet die Folge, bei der man stehengeblieben ist —
über dieselbe Frage, die auch die Startseite stellt. Gibt es keine, wird
es die erste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Es fehlte der erste Schritt. Die Stream-Adresse wurde selbst
zusammengebaut, und damit fehlten PlaySessionId, die ausgehandelte
MediaSourceId und die Adresse, die der Server eigentlich liefert. Im
Swift-Code steht seit jeher, warum das nicht geht: "eine selbst
zusammengebaute Adresse wäre eine Vermutung darüber."
Jetzt vier Schritte wie in der App: PlaybackInfo mit Geräteprofil, die
Quelle aus der Antwort wählen, die Adresse des Servers nehmen, die
Sitzungskennung mitführen.
Vor allem aber sind Fehler nicht mehr unsichtbar. Vorher endete jeder
Fehlschlag in einem stummen schwarzen Kasten — ein 401 sah aus wie ein
404 und wie ein Netzproblem. Geprüft: Ablehnung, fehlende Quelle und
nicht abspielbares Format nennen jetzt alle drei Ross und Reiter.
Serien und Staffeln haben keinen Abspielknopf mehr. Sie sind Ordner;
/Videos/{serienId} zeigt auf nichts. Wer über eine Serienkachel getestet
hat, konnte gar nichts starten.
Dazu Fortschrittsmeldung an /Sessions/Playing — ohne die erscheint der
Titel auf dem Server nie als "läuft gerade", und "Weiterschauen" bliebe
im Web für immer leer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 3 des Abgleichs. Dieselbe Rechnung wie Arrangement in der App,
samt ihrer wichtigsten Eigenschaft: unbekannte Reihen landen am Ende,
statt zu verschwinden. Sie zu verwerfen wäre die einfachere Regel — und
der Grund dafür, dass eine neu angelegte Mediathek nie aufgetaucht wäre.
Ausgeblendete stehen in den Einstellungen weiter unten in der Liste,
durchgestrichen. Ohne das könnte man sie nie wieder einblenden.
Knöpfe statt Ziehen: auf dem Zeigegerät ist Ziehen schöner, auf einem
Trackpad fummelig und mit der Tastatur gar nicht zu machen. Zwei Pfeile
können alle drei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein reiner Weg von A nach B sah aus, als würde ein Kasten verschoben.
Jetzt zwei Züge: die Leiste zieht sich erst in sich zusammen und zieht
dann erst um — wie ein Gummiband, das sich sammelt, bevor es schnellt.
Welche Richtung sich zuerst sammelt, hängt davon ab, wohin es geht. Wer
nach oben zieht, wird flach, also klappt zuerst die Höhe zusammen; wer
an die Seite zieht, wird schmal, also zuerst die Breite. Die
Zwischenstation steht deshalb noch am alten Ort, aber schon im Zielmaß
der einen Seite.
Nachgemessen: bei 220 ms ist die Breite von 2.27 auf 1.38 gefallen und
die Position unverändert — sie sammelt sich, ohne zu wandern. Bei 380 ms
ist sie unterwegs und zieht sich auf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 2 des Abgleichs. "Plakate oder Querformat" fliegt raus — das ist
keine Einstellung, sondern eine Entscheidung, die die App bewusst selbst
trifft: Angefangenes erkennt man am Szenenbild wieder, Neues sucht man
nach Plakaten aus. Ich hatte sie zur Wahl gemacht, ohne dass jemand
danach gefragt hätte.
Dafür die Abschnitte in der Reihenfolge der App — Darstellung,
Startseite, Server — und die eigene Akzentfarbe als Hex-Feld, mit
derselben Kontrastrechnung nach WCAG 2.1. Nachgerechnet: ein dunkles
Marineblau landet auf genau 4.5, dem Grenzwert, und eine Farbe, die ihn
ohnehin erreicht, bleibt unberührt. Gesagt wird es auch — eine Farbe
stillschweigend zu verändern wäre schlechter, als sie abzulehnen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1 des Einstellungs-Abgleichs. Beide standen bisher nur im Web,
obwohl sie nirgends hingehören, wo es die Sache nicht überall gibt.
Die Kachelgröße ist ein Faktor und keine Punktzahl: die Breiten von
Plakat und Querformat stehen in einem Verhältnis, das stimmen soll —
zwei getrennte Zahlen ließen sich gegeneinander verstellen, bis eine
Reihe zur anderen nicht mehr passt. Sie wirkt auf Startseite und
Mediathek, sonst sähe das eine anders aus als das andere.
Sie steht bei der Darstellung, nicht bei der Startseite: es geht ums
Aussehen, und sie gilt für beide Seiten. Die Aufmacherhöhe steht bei der
Startseite, direkt bei Quelle und Anzahl.
Voreingestellt ist genau das bisherige feste Verhalten — wer nichts
anfasst, merkt nichts. Gelesene Werte werden auf ihren Bereich begrenzt,
damit eine ältere Ablage keine Kachel über den halben Bildschirm ergibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der erste Versuch klappte die Spalte nach oben weg und schob die Insel
seitwärts herein. Das sind zwei Leisten, von denen eine geht und eine
kommt — Mac und iPad lassen dieselbe umziehen, und man verfolgt sie
dabei mit den Augen.
Jetzt FLIP: alte Lage messen, umbauen, neue Lage messen, und das Element
vom alten in den neuen Zustand fahren lassen. Die Größe fährt mit, denn
die Insel ist oben breit und flach und an der Seite schmal und hoch; ein
Sprung in der Form mitten in einer ruhigen Bewegung fällt mehr auf als
gar keine Bewegung. Über CSS allein ginge das nicht — "right: auto" und
"bottom: auto" lassen sich nicht animieren, und genau die wechseln hier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nachgebaut nach der App: die Spalte klappt nach oben weg und kommt von
oben zurück, die Insel schiebt sich seitwärts hinaus und herein. Der
Inhalt rückt gleichzeitig nach — ohne das bliebe links ein Loch stehen,
bis die Leiste angekommen ist.
Dazwischen eine Pause von gut zwei Zehnteln: ohne sie tauscht der
Browser die Leiste in einem Bild aus, und vom Umzug ist nichts zu sehen.
Dazu gestaffelt einblendende Reihen und Kacheln, die sich beim
Überfahren leicht anheben. Wer "Bewegung reduzieren" eingeschaltet hat,
bekommt von alledem nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan für den Abgleich zwischen App und Web. Beim Bau des Webfrontends
sind zwei Einstellungen entstanden, die es in der App nicht gibt, und
eine dritte, die ihrer Gestaltung widerspricht: Plakate oder Querformat
entscheidet die App bewusst selbst, weil man Angefangenes am Szenenbild
wiedererkennt und Neues am Plakat aussucht.
Gleiche Gliederung, Werte pro Gerät — am Fernseher größere Kacheln als
am Laptop, ohne dass eins das andere umstellt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wie auf dem iPad: ein Knopf in der Leiste selbst — an der Seite oben in
der Ecke, oben ganz links. Ihn in die Einstellungen zu verbannen hieß,
für einen Handgriff zwei Ebenen tief zu gehen.
Dazu die Einstellungen, die bisher aus vier Zeilen bestanden: Aufmacher
(Quelle, Anzahl, Höhe), Reihen (Weiterschauen zusammenlegen, Gesehenes
ausblenden, Plakate oder Querformat), Kachelgröße, Akzentfarbe, Server.
Sie wirken auch — die Startseite baut sich danach auf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Er war nun einmal der erste in "Weiterschauen", und genommen wurden
schlicht die ersten fünf der Liste. Jetzt wird gezogen — aber nicht bei
jedem Blick: der Zufall hängt an einem Zeitfenster von sechs Stunden.
Wer die App dreimal am Abend öffnet, findet sich zurecht; wer am
nächsten Morgen wiederkommt, sieht etwas anderes.
Titel ohne Hintergrundbild sind dabei nicht ausgeschlossen, sondern
ziehen ein schlechteres Los. Sie ganz auszusperren war zu hart — in
einer Mediathek, in der wenige Titel eines tragen, blieben immer
dieselben drei übrig.
Dieselbe Regel im Web, damit beide nicht auseinanderlaufen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Dinge aus einer Runde Rückmeldungen.
Die Detailseiten forderten ihr Bild in 900×1200 an — hochkant — und
zeigten es in einem 16:9-Rahmen; zu sehen war ein Streifen aus der
Mitte. Derselbe Fehler war im Aufmacher schon einmal behoben worden, nur
hier nicht. Beide gehen jetzt über HeroArtwork, wo die richtigen Maße
schon stehen.
Im Web hingen die Karussellpunkte neben dem Aufmacher, das CSS zielte
aber hinein — sie waren unsichtbar. Jetzt sind sie da, anklickbar, und
es gibt Pfeile, Wischen und Pfeiltasten dazu. Wer selbst blättert, hält
das Weiterwechseln an: sonst springt der Aufmacher genau in der Sekunde,
in der man nach "Abspielen" greift.
Dazu Seitenleiste und Einstellungen fürs Web — Akzentfarbe aus derselben
Palette wie die App, Navigation oben oder seitlich, und ein Regler für
die Kachelgröße. Das Filmlogo ist um zehn Prozent gewachsen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auf einem großen Fenster blieb die zweite Reihe fast vollständig grau,
während die erste geladen hatte. Es lag weder am Server noch an
fehlenden Bildern: sechzig sichtbare Kacheln teilen sich zwölf
Verbindungen, und die hinteren stehen so lange in der Schlange, dass sie
in den Zeitablauf laufen. Oben fragt niemand ein zweites Mal — task(id:)
läuft einmal je Adresse.
Jetzt wird bis zu dreimal gefragt, mit wachsendem Abstand. Aber nur, wo
es sich lohnt: ein 404 heißt, dass es das Bild nicht gibt, und dreimal
zu fragen macht es nicht vorhanden. Wiederholt werden Zeitabläufe,
abgerissene Verbindungen, 5xx und 429.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Datei, die gegen Jellyfin spricht: Anmeldung, Startseite mit
Aufmacher und Reihen, Mediathek als Raster, Wiedergabe über HLS. Die
Endpunkte sind aus JellyfinKit übernommen, nicht geraten — inklusive der
Anführungszeichen in der Authorization-Zeile, ohne die Jellyfin die Zeile
falsch zerlegt.
Gestaltet aus denselben Werten wie die App: Flächen, Radien, Abstände aus
FlixyDesign. Die Navigationsinsel schwebt über einem randlosen Aufmacher
und fährt beim Scrollen weg — dieselbe Entscheidung wie dort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Für den Zugang von Windows aus, wo es keine App gibt und auch keine
geben wird. Ein Kleid, kein Umbau.
Variablennamen und Klassen stammen aus einer laufenden Jellyfin-Instanz,
nicht aus dem Gedächtnis: Jellyfin baut auf einer MUI-Palette auf, und
wer die umbiegt, erwischt auch Bauteile, die dieses CSS gar nicht kennt.
Direkt überschrieben wird nur dort, wo Jellyfin an der Palette vorbei
setzt — der Seitenhintergrund und die Kartenrundung tun das.
Live gegen die öffentliche Jellyfin-Demo geprüft: Farben, Systemschrift,
Kartenrundung, Akzent und die schwebende Kopfleiste greifen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EpisodeTitle statt Name: hier stand der Reihentitel ein zweites Mal, und
im Sendungsblatt hätte derselbe Satz zweimal untereinander gestanden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der dritte Modus neben Raster und Senderliste — und der einzige, den es
nicht überall gibt. Bei Plex fehlt schlicht der Schalter dorthin, denn
supportsRecordings fragt die eigene Fähigkeit ab.
Vormerken sitzt im Blatt, das eine angetippte Sendung ohnehin schon
öffnet, statt in einem zweiten Kontextmenü daneben. Trägt die Sendung
bereits einen Serien-Timer, steht dort das statt der Knöpfe: zweimal
vormerken ist kein Wunsch, sondern ein Versehen.
Aufnahmen führen über die Detailseite statt unmittelbar in den Player.
Eine Aufnahme ist auf dem Server ein gewöhnlicher Eintrag, und erst dort
steht, in welchen Fassungen sie vorliegt — von der Liste aus zu starten
hieße, das zu raten.
Zwölf Schlüssel in DE und EN. Die Übersetzungsdatei ist dabei auf das
Format zurückgeschrieben worden, das auch im letzten Commit steht;
inhaltlich ist sie HEAD plus diese zwölf, nachgerechnet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keine Datei unter Views/LiveTV importiert noch PlexKit. Sender, Programm
und Einschalten laufen über den neutralen Vertrag, und supportsLiveTV
fragt endlich die Fähigkeit ab statt "server != nil" — womit die Zusage
in JellyfinBackend.capabilities zum ersten Mal stimmt.
Der Player holt die laufende Sendung jetzt für genau einen Sender nach.
Vorher ging dafür das Raster über alle Sender hinaus, weil der Plex-Weg
keinen schmaleren kannte; im neutralen Vertrag ist die Senderliste ein
Parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Protokolle statt eines: LiveTVBackend für Sender, Programm und
Einschalten, LiveTVRecordingBackend für Aufnahmen. Getrennt, weil Plex
das eine kann und das andere nicht — ein as? sagt die Wahrheit, eine
Methode, die bei Plex nur wirft, verspricht etwas und bricht es dann.
Plex zieht um, ohne umgebaut zu werden. Der Suchlauf, der die
Senderliste findet, bleibt Zeile für Zeile, wo er ist; davor liegt jetzt
ein Actor, der sich Empfänger und Sender merkt. Nötig, weil PlexBackend
ein struct ist und tune() den echten PlexChannel braucht — ohne
Gedächtnis liefe der Suchlauf bei jedem Antippen neu.
Jellyfin ist neu und deutlich kürzer, weil es dort nichts zu suchen
gibt. Die Endpunkte stammen aus der OpenAPI-Spec des Servers. Beim
Vormerken wird die Vorlage des Servers roh weitergereicht statt
entschlüsselt und neu zusammengesetzt: welche Felder Pflicht sind, sagt
die Beschreibung nicht, und ein verlorenes fiele erst an einer Aufnahme
auf, die nicht startet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec für Jellyfin-Live-TV. Der eigentliche Befund ist nicht, dass
Jellyfin fehlt, sondern dass das Feature quer an der Abstraktion vorbei
gebaut ist: die Views importieren PlexKit unmittelbar und sprechen über
session.server statt über session.backend.
Die Jellyfin-Endpunkte stammen aus der OpenAPI-Spec des Servers, nicht
aus dem Gedächtnis — in genau dieser Ecke steht, was dreimaliges Raten
auf einem echten Server gekostet hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der schwarze Streifen über der Navigation war kein gezeichneter Balken,
sondern fehlender Inhalt: der gesamte Baum saß innerhalb des
Sicherheitsrands. Deshalb erreichte kein noch so gut gesetzter Rand
weiter unten die Fläche darüber — Inhalt kam dort nie hin.
Jetzt reißt der HStack den Rand oben auf und gibt den Platz kontrolliert
zurück: Seitenleiste und Insel bekommen ihren Abstand zur Statusleiste
explizit, der Inhalt bekommt ihn als Sicherheitsrand. Der Aufmacher
nimmt ihn sich wieder — um genau diese Strecke höher und um genau diese
Strecke größer, sodass unten alles bleibt, wo es war. Dazu ein knapper
Verlauf oben, damit Uhrzeit und Batterie auch über einem hellen Bild
lesbar bleiben.
Und die Leiste oben fährt beim Scrollen weg. Nur sie: der
Sicherheitsrand darunter bleibt stehen, sonst ruckte die Seite bei jedem
Ein- und Ausblenden unter dem Finger weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec für die randlose Navigationsinsel auf dem iPad. Der Baum sitzt
heute vollständig innerhalb der Safe Area — deshalb erreicht kein
safeAreaInset weiter unten den Streifen über der Insel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unter dem Folgentitel stand „S2 E1" als Text, und wer zur Serie wollte, musste
darunter eine Knopfreihe finden. Jetzt steht dort „Serienname · Staffel 2 ·
Episode 1", und Serienname wie Staffel führen dorthin, wohin sie zeigen. Der
Name steht ohnehin da — dann soll er auch der Weg sein.
`HeroBanner` nimmt dafür wahlweise eine fertige Zeile entgegen statt eines
Textes. Gebaut wird sie beim Aufrufer, weil nur der weiß, wohin ein Teil führt;
die Gestaltung kennt keine Ziele.
Die Knopfreihe bleibt bei Staffeln, wo sie der einzige Weg nach oben ist, und
entfällt bei Folgen.
Beim Entfernen eines Downloads gibt es keine Rückfrage mehr. Sie stand da, weil
„löschen" nach etwas Endgültigem klingt — es ist aber nur die heruntergeladene
Kopie, der Titel bleibt auf dem Server. Wer nach links wischt und auf „Löschen"
tippt, hat zweimal gesagt, was er will.
Und das Umschalten sieht jetzt nach Umzug aus: die Seitenleiste klappt nach
oben weg, die Leiste oben schiebt sich seitwärts herein — und zurück genauso.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auf dem iPad beschneidet der TabView seinen Inhalt, auf dem Mac nicht.
Gemessen: unter der Leiste war dort gar kein Bild — die Fläche blieb
gleichmäßig dunkel, während der Aufmacher daneben bei 60 lag. Der negative
Rand, mit dem der Aufmacher unter die Leiste greifen sollte, wurde schlicht
abgeschnitten, und übrig blieb der schwarze Spalt dazwischen.
Also weg damit. Die Leiste steht in einem HStack neben dem Inhalt: kein
negativer Rand, keine Maske, kein Sicherheitsrand je Bildschirm, keine
Sonderfälle für geschobene Bildschirme. Die Leiste hat ihren Abstand zu drei
Seiten und zum Inhalt keinen — dort schließt er unmittelbar an.
Die Startseite hat keine Navigationsleiste mehr. Sie war leer und belegte
trotzdem ihre Höhe; der Aufmacher fing dadurch tiefer an als die Leiste
daneben, und genau diese Kante sah man. Auf den geschobenen Bildschirmen
bleibt sie, dort trägt sie den Zurück-Weg.
Und Wischen auf dem Aufmacher schluckt das Scrollen nicht mehr. `minimumDistance: 0`
ließ die Geste sofort greifen — wer nach oben wischte, bei dem passierte
nichts. Jetzt vierundzwanzig Punkte und nur, wenn es waagerecht gemeint ist;
das Anhalten des Wechsels übernimmt eine eigene Tippgeste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Umschalter stand in einem `withAnimation`, und trotzdem sprang die Leiste
von einer Stellung in die andere. `@AppStorage` schreibt in die
Voreinstellungen und meldet die Änderung erst danach zurück — sie kommt also
außerhalb der Animation an, und die Animation hat nichts, woran sie sich
festhalten könnte.
Jetzt führt ein Zustand dieser Ansicht, und gespeichert wird hinterher. Die
Bindung an die Leisten bringt die Animation gleich mit.
Nachgesehen, nicht vermutet: mit versuchsweise zwei Sekunden Dauer und einer
Serienaufnahme von außen ist der Zwischenzustand fotografiert — Seitenleiste
halb nach links draußen, Leiste oben im Einblenden, Inhalt auf dem Weg. Danach
zurück auf knappe 0,32 Sekunden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Sicherheitsrand je Bildschirm wandert nicht in einen geschobenen Bildschirm
mit. Die Einstellungen schieben ihre Unterseiten über gewöhnliche Verweise, und
dort fehlte er: die Beschriftungen lagen hinter der Seitenleiste und wurden von
der Maske abgeschnitten — zu sehen waren nur die rechtsbündigen Werte.
Der Platz für die Leiste ist jetzt ein echter Rand am ganzen TabView. Der gilt
für alles, was darin aufgeht.
Dazu: der Aufmacher nimmt zweiundachtzig statt hundert Prozent der Höhe. Über
die ganze Höhe sah er aus wie die einzige Seite der App — von den Reihen war
ohne Scrollen nichts zu sehen. So lugt die erste gerade herein.
Und das Umschalten zwischen Seite und oben hat eine Bewegung: die Leiste kommt
von der Seite beziehungsweise von oben herein und geht dorthin zurück. Die
Überlagerung behält dabei ihre Ausrichtung — wechselte sie mit, sprang die
Leiste im Übergang an die neue Ecke, statt dorthin zu wandern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Maske hörte an der Inhaltskante auf, die Leiste endet aber zwölf Punkte
davor — dazwischen stand nichts. Jetzt reicht das Bild bis an ihre Kante heran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Seitenleiste lässt sich wieder bearbeiten: Ziehen zum Umsortieren, Haken
zum Ein- und Ausblenden. Das Anordnen macht `Arrangement` aus MediaKit — dieselbe
Rechnung samt Tests, die schon die Reihen der Startseite ordnet, mit ihren
kniffligen Fällen: eine Mediathek, die neu auftaucht, und eine, die aus der
gespeicherten Reihenfolge verschwindet. Gespeichert wird über eine feste
Kennung **ohne** den Titel, damit eine umbenannte Mediathek ihren Platz behält.
Die Suche ist auf dem iPhone kein eigener Reiter mehr. Die Leiste unten hat
fünf Plätze, und einer davon für etwas, das man in einer Mediathek sucht, ist
einer zu viel — sie sitzt jetzt über der Mediathek, neben Filter und Ansicht.
Der Umschalter zwischen Seite und oben ließ sich auf dem Mac kaum treffen: ein
Knopf aus einem Zeichen mit Rand hat ohne `contentShape` nur das Zeichen als
Ziel. Beide Umschalter haben jetzt eine Fläche.
Die Leiste oben trägt dieselbe Schriftgröße wie die Seitenleiste. In
Beschriftungsgröße war sie auf einem großen Bildschirm winzig.
Und eine Maske: der Aufmacher greift nach links unter die Seitenleiste, scrollt
dabei aber, während die Leiste steht — über und unter ihr blieb je ein Streifen
Bild stehen. Die Maske sitzt im festen Teil und lässt genau zwei Flächen übrig:
alles ab der Inhaltskante, und die Fläche der Leiste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein negativer Rand dehnt, was er umschließt, meldet nach außen aber die alte
Breite. Er stand ganz oben, also legten sich Verlauf und Schrift nur über den
alten Bereich — und die zwölf Punkte daneben blieben ohne Verlauf und damit
hell. Gemessen: dort 60 Helligkeit, im Inhaltsbereich 19, bei gleichem Bild.
Jetzt wird erst der Verlauf gelegt, dann gedehnt, dann die Schrift gesetzt. Der
Verlauf deckt bis zum Rand, die Schrift bleibt auf der gemeldeten Breite und
fängt von selbst dort an, wo auch die Reihen darunter anfangen.
Der Weg dorthin ging über drei Irrtümer, die alle widerlegt sind: nicht
`glassEffect` (der Streifen blieb bei undurchsichtiger Fläche), nicht der
Platzhalter (er blieb bei grünem Platzhalter), nicht der Scroll-Anzeiger (er
blieb ohne Bildlaufansicht). Die Bildlaufansicht in der Leiste ist trotzdem
draußen — sie wurde nicht gebraucht.
Dazu: der Aufmacher nimmt die volle Höhe statt achtundachtzig Prozent, der
Haarstrich der Leiste ist zurückhaltender, und auf dem iPhone ist der Aufmacher
das **Plakat** statt des Hintergrundbilds. Ein 16:9-Bild hochkant zeigt einen
Ausschnitt aus der Mitte; ein Plakat ist von vornherein hochkant und passt
ohne Beschnitt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Aufmacher nimmt jetzt achtundachtzig Prozent der sichtbaren Höhe, statt
sich seine Höhe aus 16:9 auszurechnen. Ein Aufmacher, der ein Drittel hoch ist,
sieht aus wie ein breites Vorschaubild; die zwölf Prozent Rest sind Absicht,
dort lugt die erste Reihe herein und sagt, dass es weitergeht. Auf schmalen
Geräten bleibt es beim Hochformat.
`FlixyArtwork` kann dafür den Rahmen ausfüllen, den es bekommt. Das
Seitenverhältnis bleibt trotzdem wichtig — danach werden die Bilder beim Server
angefordert.
Und `glassEffect` ist raus. Er zeichnet an seiner Kante einen Lichtsaum, und
über einem hellen Aufmacher wurde daraus ein heller Balken von mehreren Punkten
Breite neben der Leiste — kein Rand, sondern ein Streifen. Von einer Kante war
gleichzeitig nichts zu erkennen.
Von Hand sind es drei Lagen: ein trübes Material, ein Schleier gegen die
durchziehende Startseite, und ein Haarstrich, der oben links aufhellt und unten
rechts nachlässt. Dazu ein Schatten — was schwebt, wirft einen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Dinge, die zusammengehören:
Die Spiegelung ist weg. `backgroundExtensionEffect` füllt die Fläche unter der
Leiste, indem er den Bildrand gespiegelt weiterzeichnet — und je größer die zu
füllende Fläche wurde, desto mehr übernahm der Spiegel das Bild. Auf dem iPhone
legte er sich ohne jede Seitenleiste über den Statusbalken. Jetzt läuft das
Bild selbst weiter, über einen negativen Rand.
Es endet dabei an der **Kante der Leiste**, nicht am Fensterrand. Der
Unterschied sind zwölf Punkte, und die waren deutlich zu sehen: neben der
schwebenden Leiste stand ein heller Streifen Bild, den nichts abdeckte.
Das Seitenverhältnis im Karussell musste weichen. Es legte den Aufmacher auf
die Breite des Inhaltsbereichs fest; weil er nach links unter die Leiste
greift, blieb rechts genau so viel schwarz übrig, wie er links gewann.
Und die Maße, in denen das Hintergrundbild angefordert wird, richten sich jetzt
nach dem Rahmen, in dem es landet. Vorher wurden 900×1200 geholt — hochkant —
und in einem 16:9-Rahmen gezeigt: zu sehen war ein Streifen aus der Mitte, oben
und unten fehlte je ein Drittel.
Dazu: auf schmalen Geräten ist der Aufmacher hochkant. In 16:9 blieben auf
einem iPhone keine zweihundertzwanzig Punkte Höhe, und Schriftzug, Zeile,
Beschreibung und zwei Knöpfe lagen übereinander.
Und ein Schleier unter dem Glas der Leiste. Reines Glas zeigt alles, was
dahinter vorbeizieht, und beim Scrollen zieht die ganze Startseite vorbei — aus
den ruhigen Beschriftungen wurde ein unruhiger Brei. Der Schleier nimmt der
Bewegung die Schärfe, ohne die Fläche zuzumauern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sie auszublenden reichte nicht: solange `mode` auf `tabSidebar` steht, gehört
die Seitenleiste zur Darstellung, und das System stellte sie wieder her — mit
eigenem Pfeil und eigenem Knopf, hinter unserer. Auf dem Simulator fiel das
nicht auf, auf dem Mac standen beide da.
Jetzt wird erst die Form auf `tabBar` gestellt und dann die Tableiste
ausgeblendet. Damit gibt es keine Seitenleiste mehr, die zurückkommen könnte.
Der Test setzt die Stellung der Leiste über ein Startargument, statt zu nehmen,
was ein voriger Lauf hinterlassen hat — er fing sonst beim falschen Bild an.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die System-Seitenleiste sitzt auf dem iPad als schwebende Fläche und auf dem
Mac angeklebt am Fensterrand. `preferredLayout = .overlap` ist die dafür
vorgesehene Bitte, und macOS verwirft sie stillschweigend — gemessen, nicht
vermutet.
Statt deshalb die ganze Navigation neu zu bauen, bleibt der `TabView` stehen
und verliert nur sein Kleid: `TabChromeSuppressor` blendet Seiten- und
Tableiste aus, gezeichnet wird darüber. Auswahl, ein eigener Navigationsstapel
je Eintrag, erhaltener Scrollstand beim Wechseln — all das macht weiter das
System. Dass das Ausblenden geht, war kein Ratschluss: derselbe Griff hatte die
Leiste vorher schon versuchsweise verschwinden lassen.
Neu sind `FlixySidebar` (schwebende Fläche mit eigenem Rand, Mediatheken
einklappbar, Konto unten) und `FlixyTopBar` (dieselben Einträge als Leiste
oben, Mediatheken als Menü, weil oben der Platz waagerecht begrenzt ist). Das
iPhone bleibt beim System: dort gibt es keine Seitenleiste, sondern die Leiste
unten, und die ist überall dieselbe.
Der Platz für die Leiste ist ein **Sicherheitsrand**, kein Abstand, und er
hängt je Bildschirm — weder `TabView` noch `NavigationStack` reichen ihn an
ihren Inhalt weiter. Damit bleibt eine Reihe fensterbreit und schiebt ihre
Kacheln beim Wischen hinter die Leiste, stellt aber im Ruhezustand keine
dorthin. Der Aufmacher darf über seinen Bereich hinaus; das `.clipped()` im
Karussell musste dafür weichen, es schnitt genau das ab.
Noch nicht dabei: „Bearbeiten" mit Ziehen und Ein-/Ausblenden. Die Einträge
stehen in fester Reihenfolge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Kreuzblenden sind für einen Moment beide Bilder halbdurchsichtig — und
dazwischen scheint der Untergrund durch. Das war das helle Aufblitzen des
vorigen Titels.
Jetzt bleibt das alte Bild voll deckend liegen, und das neue legt sich darüber,
während es hereinrutscht. Erst wenn oben nichts mehr durchscheint, verschwindet
unten etwas — die Fläche ist zu keinem Zeitpunkt durchsichtig.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Schriftzug hing weiter nicht an der Animation, und der Zwischenspeicher war
nicht schuld: er lag im Actor, und ein `await` kostet immer mindestens einen
Durchlauf. Das neue Bild erschien deshalb im ersten Bild ohne Schriftzug und im
zweiten mit — er sprang also getrennt herein, obwohl er längst da war.
Der Speicher liegt jetzt außerhalb des Actors und lässt sich beim Zeichnen
lesen. Das gilt für alle Bilder: auch eine zurückscrollende Kachel blitzt nicht
mehr kurz grau auf.
Zur Seitenleiste auf dem Mac, gemessen statt vermutet: `automatic` löst sich
laut Apples eigener Beschreibung auf iOS zu `overlap` auf (die Leiste schwebt
über dem Inhalt) und auf macOS zu `tile` (sie schiebt ihn zur Seite und klebt
am Fensterrand). Der Griff dorthin sitzt richtig — derselbe Weg hat die Leiste
auf Kommando ganz ausgeblendet —, aber `overlap` wird auf dem Mac
stillschweigend verworfen. Die Bitte bleibt trotzdem stehen; sie kostet nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`AsyncImage` bringt seine eigene Einblendung mit und fängt erst an zu laden,
wenn es erscheint. Beim Überblenden des Aufmachers blendete sich der Schriftzug
des Films deshalb getrennt und später ein, während Titelzeile, Beschreibung und
Knöpfe längst mitgewandert waren — zwei Bewegungen, wo eine sein soll.
Jetzt lädt er über denselben Zwischenspeicher wie alle anderen Bilder, und das
Karussell holt ihn zusammen mit dem Hintergrundbild vor. Beim Erscheinen liegt
er bereit und macht mit, was der Aufmacher macht.
Dazu ein kleines Hilfsprogramm, das die Fensterkennung eines laufenden
Programms ausgibt, damit `screencapture -l` genau dessen Fenster fotografiert
und sonst nichts vom Bildschirm. Das System-Python bringt PyObjC nicht mehr
mit, und ohne Kennung gibt es keinen Weg dorthin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der gespiegelte Ausschnitt hinter der Seitenleiste war nicht das Problem —
JellyTV macht es genauso. Auffällig wurde er erst durch die Bewegung: beim
seitlichen Durchblättern wanderten das echte und das gespiegelte Stück
gegeneinander, und die Naht dazwischen war das auffälligste Ding auf dem
Bildschirm.
Jetzt rutscht das neue Bild 32 Punkte herein und blendet dabei über das alte.
Nichts wandert weit genug, als dass ein Versatz auffiele. Wischen blättert
weiter, nach vorn wie zurück; das nächste Bild wird geholt, solange das
aktuelle noch steht, sonst blendet der Aufmacher auf eine graue Fläche über.
Dazu ein Testziel, das die App mit der Attrappe in einen Zustand fährt und
fotografiert. Der Grund ist ein wiederkehrender Fehler: Aussagen über das
Aussehen waren Vermutungen, weil niemand hinsehen konnte — der Simulator kennt
keinen Befehl fürs Antippen, und dieses Xcode bringt kein Simulator-Fenster
mehr mit. Damit ist „sieht es so aus?" eine Frage, die sich beantworten lässt.
Die Startargument-Anmeldung an der Attrappe steht unter #if DEBUG und führt am
Schlüsselbund vorbei: kein echter Server, kein echtes Konto, kein echter Token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beide App-Ziele lesen denselben Ordner ein. XcodeGen nimmt nur die eigene
Info.plist aus den Quellen heraus — die des jeweils anderen Ziels bleibt drin
und landet unverarbeitet als Ressource im Bündel, mitsamt dem wörtlichen
$(PRODUCT_BUNDLE_IDENTIFIER).
Wer die Datei danach liest, liest die falsche: codesign stempelte
Identifier=$(PRODUCT_BUNDLE_IDENTIFIER) auf die App, und das iPad lehnte sie
als „not a valid bundle" ab. „Clean Build Folder" konnte daran nichts ändern —
der Rat war falsch, der Fehler kam aus project.yml.
Dazu: Bilder laden jetzt über ArtworkLoader statt AsyncImage. AsyncImage lädt
bei jedem Erscheinen neu und kennt keinen Zwischenspeicher; bei sechzig Kacheln
plus einem großen Aufmacher stauen sich die Anfragen hinter sechs Verbindungen.
Auf dem iPad fiel das nicht auf, auf dem Mac blieb der Aufmacher grau.
Und die beiden #if os(macOS) um die Breite der Seitenleiste liefen unter
Catalyst ins Leere — dort ist os(iOS) wahr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler auf einmal, und alle drei meine.
**Die App kam gar nicht ins Netz.** Ein Catalyst-Programm läuft in der Sandbox
und braucht eine ausdrückliche Erlaubnis für ausgehende Verbindungen. Ohne sie
gibt es keine Fehlermeldung — der Ladekreis dreht sich einfach für immer, und
genau so sah es aus.
**In der Seitenleiste standen Schlüssel statt Wörtern** — „sidebar.downloads"
statt „Downloads". Ich hatte zwei Fassungen derselben Beschriftung gebaut, eine
mit `LocalizedStringKey` und eine mit `String` für Titel, die vom Server
kommen. Bei einem Literal darf Swift sich für beide entscheiden, und es
entschied sich nicht überall gleich. Jetzt gibt es eine Fassung; wer einen
fertigen Text hat, baut sein `Text` selbst.
**Das Fenster öffnete wieder in Notizzettelgröße.** Meine Größenangabe hing an
`os(macOS)` — unter Catalyst gilt aber `os(iOS)`, also griff sie nicht. Dort
hängt die Größe ohnehin an der Szene, nicht am Fenster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 Untergrenze allein war die halbe Arbeit. Eine Spalte, die sich nach ihrem
Inhalt richtet, lässt sich ohne Deckel über das halbe Fenster ziehen — und
genau das ging.
Beide Zahlen stehen jetzt an einer Stelle: 190 Punkt schmal genug, dass die
Leiste nicht bläht, 320 breit genug für lange Mediatheksnamen, und das ist
zugleich die Breite, die eine Seitenleiste auf dem iPad hat. Die Beschriftungen
tragen dieselben Grenzen, damit die Spalte nicht doch wieder an einem langen
Wort hängen bleibt.
Angefasst wird die Breite über den Inhalt der unteren Leiste — die sitzt in der
Seitenleiste, also nimmt diese ihre Angabe an. Einen unmittelbaren Griff an die
Spalte gibt es bei dieser Navigationsform nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Gründe, warum der Mac anders aussah, und keiner davon war Geschmack.
**Die Schrift.** macOS rendert dieselben Textstile deutlich kleiner als iOS:
`largeTitle` misst dort 26 Punkt statt 34, Fließtext 13 statt 17. Eine
Oberfläche, die auf dem iPad stimmt, wirkt damit zusammengeschrumpft. Auf dem
Mac stehen jetzt feste Größen — die von iOS. Das kostet dort Dynamic Type;
vertretbar, weil macOS keine systemweite Textgröße kennt und man ein Fenster
stattdessen größer ziehen kann.
**Der Aufmacher wächst mit.** Das war der zweite, größere Teil: das Bild ist so
breit wie das Fenster, die Schrift darauf war es nicht. Bei 1280 Punkt Breite
wirkt ein 34-Punkt-Titel verloren, bei 834 auf dem iPad genau richtig. Titel,
Schriftzug, Angaben und Abstände richten sich jetzt nach der Breite des
Aufmachers — mit dem iPad als Bezug und einem Deckel, damit ein Fenster über
die ganze Wand nicht in Plakatschrift endet. Die Beschreibung bekommt
außerdem eine Zeilenbreite: ein Satz über 1280 Punkt ist unlesbar, das Auge
verliert die Zeile.
**Die Seitenleiste.** Sie richtet sich nach ihrem Inhalt, und bei Wörtern wie
„Start" fällt sie auf eine Breite zusammen, in der schon der Kontoname darunter
nicht mehr Platz hatte — daher das „S…". Die Beschriftungen haben jetzt eine
Mindestbreite.
**Die Formulare.** Auf dem Mac nahm SwiftUI den Spalten-Stil: Beschriftungen
rechts ausgerichtet, keine abgesetzten Abschnitte. Jetzt derselbe gruppierte
Stil wie auf iOS.
Dazu: Scrollbalken sind weg. Der graue Streifen unter dem Aufmacher kam daher,
dass in den Systemeinstellungen „Scrollbalken immer einblenden" steht —
dagegen reicht `.hidden` nicht, `.never` schon. Und das Fenster öffnet mit
1280 × 820 statt in Notizzettelgröße, ohne graue Titelleiste.
263 Tests grün, iOS baut unverändert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>