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>
Und dein Argument dafür war das beste: auf dem Mac kann ich selbst hinsehen.
Kein Simulator-Tunnel, der mitten im Prüfen abstürzt — `screencapture` auf das
Fenster der App, und zwar nur auf ihres, nicht auf den Bildschirm.
**Ein eigenes Ziel, nicht ein gemeinsames.** Die Info.plist der iOS-App ist
voller iOS-Angaben: Startbildschirm, erlaubte Ausrichtungen, Live Activities.
Die alle bedingt zu machen hieße, dass jede spätere Änderung an der iOS-Seite
über eine Stelle geht, die auch den Mac betrifft. Zwei Ziele sind ein paar
Zeilen mehr und eine Sorge weniger.
**Was den Unterschied ausmacht, steht an einer Stelle.** Ein neuer
`PlatformShims` in FlixyDesign übersetzt, was auf beiden Seiten verschieden
heißt: schmale Titelzeile, halbhohes Blatt, Eingabefelder ohne
Großschreibung, die Plätze in der Werkzeugleiste, die Zwischenablage. Zwanzig
Ansichten benutzen ihn, statt zwanzigmal `#if os(iOS)` zu tragen — sonst
wandert die Fallunterscheidung in jede neue Datei und wird beim übernächsten
Bildschirm vergessen.
Wirklich verschieden sind nur fünf Stellen: die Bildfläche (`UIView` gegen
`NSView`), der AirPlay-Knopf, die Ausrichtungssperre (auf dem Mac gibt es
nichts zu sperren), der App-Delegat für Hintergrund-Downloads und das
Anmeldefenster, das ein anderes Fenster als Anker braucht.
**Das Karussell blättert jetzt mit einem einrastenden Streifen** statt mit
`TabView(.page)` — den Seitenstil gibt es auf dem Mac nicht. Das ist nebenbei
die bessere Lösung: das verschachtelte TabView hatte die Eigenschaft, beim
Wechsel kurz zwei Seiten gleichzeitig zu zeigen.
**Was auf dem Mac fehlt und warum:** VLC. Das Binärpaket ist auf iOS
festgelegt; sein xcframework enthält zwar Mac-Teile, aber das ist eine eigene
Runde. Solange fällt die Wiedergabe dort auf den Systemplayer zurück — MKV und
DTS gehen also noch nicht. Live Activities gibt es auf dem Mac nicht, Bild-in-
Bild startet von Hand statt beim Verlassen der App.
Gesehen: die App startet, der Anmeldebildschirm steht, beide Wege sind da.
Angemeldet habe ich mich auf dem Mac **nicht** — das ginge nur, indem ich
einen Token in deinen Schlüsselbund schreibe, und dann fragt macOS dich beim
ersten Zugriff nach dem Passwort. Das machst du mit einem Klick selbst.
iOS baut unverändert, 263 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Eine Taste zum Vergrößern** unten rechts auf der Kachel. Das Antippen der
Fläche vergrößert weiterhin, aber auf derselben Fläche liegen schon das Ziehen
und das Aufziehen — und was dabei danebengeht, sieht aus wie „reagiert nicht".
Ein Knopf geht nicht daneben.
**Ton und Untertitel getrennt.** Es sind zwei verschiedene Fragen: die Tonspur
stellt man einmal am Anfang um, die Untertitel mitten im Film. In einem
gemeinsamen Blatt scrollte man jedes Mal an der anderen Liste vorbei. Das
Untertitel-Symbol ist gefüllt, solange welche laufen — so sieht man es, ohne
das Blatt zu öffnen. Und eine Auswahl schließt das Blatt jetzt selbst.
**AirPlay.** Nicht nachgebaut, sondern die Systemstelle: sie kennt Geräte, die
eine App nie aufzählen könnte. Der Knopf erscheint nur beim Systemplayer — VLC
zeichnet selbst und kann kein AirPlay, und ein Knopf, der dort ins Leere
führte, wäre schlimmer als keiner.
**Die Qualität steht als Text da**, nicht als drei Schieberegler: „1080p",
„720p", „Auto". Bei „automatisch" steht dort, was gerade tatsächlich ankommt —
das weiß nur der Player, und ihn zu fragen ist ehrlicher, als „Auto"
hinzuschreiben und die Antwort schuldig zu bleiben.
Dazu die Einstellungen mit farbigen Symbolen und einer Kopfkarte je Bereich.
Das ist nicht nur Zierde: in einer Liste aus zehn gleich aussehenden Zeilen
findet das Auge nichts wieder, und „das mit dem blauen Zahnrad" ist eine
brauchbare Beschreibung. Die Farben sind fest und ausdrücklich **nicht** die
Akzentfarbe — trüge alles dieselbe, unterschiede sie nichts mehr.
Und der Aufmacher läuft jetzt unter der Seitenleiste weiter
(`backgroundExtensionEffect`): die Ränder werden gespiegelt und weich
fortgeschrieben, während Titel und Knöpfe stehen bleiben. Vorher endete das
Bild an der Kante der Leiste, und beim Weiterwandern schob sich das nächste an
einer harten Linie ins Bild.
263 Tests grün. Player-Knöpfe und Einstellungen angesehen; den Aufmacher hinter
der Leiste konnte ich an den einfarbigen Flächen der Attrappe nicht sicher
beurteilen — der Simulator ist dabei erneut abgestürzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Simulator läuft wieder. Ich habe alles durchgeklickt, was ich dir in den
letzten beiden Runden ungeprüft gegeben habe. Es war nicht in Ordnung.
**Zwei Navigationsleisten übereinander.** Auf dem iPhone hat die Leiste unten
fünf Plätze; alles darüber schiebt das System in ein „Mehr"-Verzeichnis — und
dessen Einträge bekamen zwei Zurück-Pfeile, einen vom System und einen von
uns. Statt das zu flicken, gibt es jetzt die **Bibliothek**, die du ohnehin
wolltest: auf dem iPhone ein Eintrag, hinter dem alle Mediatheken stehen, auf
dem iPad weiter jede einzeln in der Seitenleiste. Damit bleibt die Leiste bei
fünf Einträgen, und das „Mehr" entfällt.
**Kacheln in der Mediathek öffneten nichts.** Kein Fehler, keine Meldung —
Antippen führte einfach ins Leere. Der Grund: in einem Stapel dürfen nicht
beide Arten von Verknüpfung nebeneinander vorkommen. Sobald ein Bildschirm
über eine **Ansichts**-Verknüpfung geschoben wurde, fanden die
**Wert**-Verknüpfungen darin ihr Ziel nicht mehr. Jetzt ist alles wertbasiert,
und das Ziel für Detailseiten steht **einmal je Stapel** an dessen Wurzel statt
in jedem Bildschirm einzeln.
**Der Lupenknopf in der Kopfzeile zeigte auf ein Ziel, das es nie gab.** Kein
Bildschirm hat je ein `SearchRoute`-Ziel angemeldet — der Knopf tat seit jeher
nichts. Er ist weg; die Suche hat ihren eigenen Platz in der Leiste.
**Der Download-Dialog zeigte Schlüssel statt Namen** — „quality.hd" statt
„720p". Die drei Übersetzungen fehlten schlicht.
**Die Kontokarte wuchs aus dem Bild heraus.** Die Leiste unten in der
Seitenleiste hat eine feste Höhe; meine aufklappende Karte schob die Zeile mit
dem Namen nach unten hinaus. Jetzt hängt sie als Popover darüber — die Zeile
bleibt stehen, wie bei JellyTV.
Verkleinerte Downloads bei Jellyfin laufen: Stufen erscheinen, die Adresse
stimmt, die Datei kommt an. Dass die Attrappe nur 28 kB liefert und die App
das als „keine Videodatei" zurückweist, ist richtig so — die Prüfung tut, was
sie soll.
Zwei Gruben in der Attrappe nebenbei gestopft: sie kannte `/stream.mp4` nicht
und meldete ihr Testbild als 360p, womit es gar keine kleineren Stufen geben
konnte.
263 Tests grün. Diesmal alles angesehen: iPhone und iPad, Einstellungen,
Bibliothek, Detailseite, Download-Dialog, Kontokarte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deine Einstellungs-Bilder sind eine lange Liste. Ich habe sie durchgegangen
und getrennt: was etwas steuert, ist gebaut; was eine Einstellung für eine
Sache wäre, die es nicht gibt, ist nicht gebaut. Ein Schalter, der nichts
bewirkt, ist schlimmer als kein Schalter.
**Die Einstellungen sind jetzt eine Übersicht.** Vorher stand alles
untereinander auf einem Bildschirm — Serverangaben, Qualität, Wiedergabe, drei
Verweise, Downloads, Version, Abmelden. Das war beim Bauen bequem und beim
Suchen anstrengend. Jetzt führt jede Zeile in einen Bildschirm, der zu einer
Frage gehört: Server, Erscheinungsbild, Startseite, Wiedergabe, Ton &
Untertitel, Downloads, Diagnose.
**Neu, und jedes davon greift irgendwo:**
- Zurück- und Vorspulen getrennt einstellbar. Die Knöpfe im Player und die
Doppeltipp-Flächen tragen danach auch das passende Symbol.
- Startgeschwindigkeit — die Engine kann es längst, gefragt wurde nie.
- „Im Querformat sperren" abschaltbar. Für die allermeisten Filme richtig, für
den, der im Bett auf der Seite liegt, das Gegenteil.
- „Gewählte Qualität merken": die Stufe gilt dann auch für den nächsten Titel.
Aufbewahrt wird eine **Zahl**, keine Sprosse — welche Sprossen es gibt, hängt
von der Quelle ab, und eine 720p-Datei kennt keine Zwanzig-Megabit-Stufe.
Dann kommt die nächstgelegene, nicht „automatisch".
- „Bitrate im Qualitätsmenü zeigen".
- Gleichzeitige Downloads. Der Wert gab es schon in der Engine, nur ohne
Bedienung und ohne dass er einen Neustart überlebte.
- Aufmacher: wie viele Titel, und ob sie aus Angefangenem oder nur aus
Neuzugängen kommen.
- „Weiterschauen und Als Nächstes zusammenlegen" und „Gesehenes aus
Neuzugängen ausblenden" — beides in der Ansicht, nicht im Server: das sind
Fragen der Darstellung, und so gelten sie für Plex und Jellyfin gleich.
**Verkleinert herunterladen geht jetzt auch bei Jellyfin.** Da hatte ich zu
früh aufgegeben: ich hatte nur nach der Adresse zum *Abspielen* gesucht, und
die ist eine Abspielliste mit hunderten Schnipseln — als Datei unbrauchbar.
`/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als eine Datei heraus. Die
Größe bleibt eine Schätzung; eine Umrechnung, die erst beim Abruf beginnt,
kennt ihre eigene Länge nicht.
**Die Kontozeile ist jetzt eine Karte, die nach oben aufklappt**, kein
Systemmenü mehr. Ein Menü legt sich als eigene Ebene über alles und springt
dorthin, wo Platz ist; die Karte bleibt, wo sie ist, und lässt die Zeile mit
dem Namen stehen — man sieht also weiter, um wessen Konto es geht.
**Nicht gebaut, mit Grund:** Trailer im Karussell, Poster- und Sammlungsstil,
Untertitel-Aussehen, sekundäre Untertitel, lokale Intros, Modular Home,
App-Symbol. Für all das fehlt das, was die Einstellung steuern würde — es gibt
in Flixy weder Trailer noch Sammlungen, und der Player zeichnet Untertitel
nicht selbst.
263 Tests grün. **Nicht angesehen**: der Simulator-Tunnel ist weiterhin
abgestürzt und ließ sich nicht wieder aufbauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Dinge aus deinen Einstellungs-Bildern.
**Das Konto sitzt jetzt unten in der Seitenleiste**, nicht als gleichrangiger
Eintrag zwischen den Mediatheken. Dort sucht man es, und von dort führt der
Weg in die Einstellungen und zum Abmelden. Auch das ist eine Systemstelle —
`tabViewSidebarBottomBar` —, also dieselbe Zeile, an der sie bei JellyTV
steht. Auf dem iPhone gibt es sie nicht; dort bleibt es beim eigenen Eintrag.
Der Buchstabe auf der Scheibe ist kein Platzhalter für ein Bild, das noch
kommt: Jellyfin gibt kein Profilbild heraus, solange keines hinterlegt ist,
und Plex kennt den Kontonamen am Server gar nicht. Bei Plex steht deshalb der
Servername oben und darunter nichts — statt zweimal dasselbe.
**Ton- und Untertitelsprache lassen sich jetzt einstellen.** Beim Durchsehen
deiner Bilder ist mir aufgefallen, dass es diese Vorliebe längst gibt: der
Player liest sie bei jeder Wiedergabe aus und wählt die passende Spur. Nur
äußern konnte man sie nirgends — sie stand in den Voreinstellungen und blieb
dort für immer leer. Eine Vorliebe, die niemand äußern kann, ist keine.
Fünfzehn Sprachen, keine vollständige Liste: ein Rad mit zweihundert
Einträgen, um „Deutsch" zu finden, wäre keine Hilfe. Was fehlt, kommt später
mit einem Suchfeld dazu.
Gebaut und übersetzt, aber **nicht angesehen**: der Simulator-Tunnel ist
mitten im Prüfen abgestürzt und ließ sich nicht wieder aufbauen. Dass es die
Systemstelle gibt, beweist der Compiler; wie sie aussieht, habe ich diesmal
nicht gesehen.
258 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auf deinem vierten Bild steht „Ziehe Objekte zur Seiten- oder Tableiste, um
sie anzupassen". Das ist kein Text, den jemand geschrieben hat, sondern einer
von Apple. JellyTV benutzt dort `TabView` mit `sidebarAdaptable` — und damit
ist die Frage, wie die das hinbekommen, beantwortet: gar nicht, das System
macht es.
Meine drei selbstgebauten Formen fliegen dafür raus: das ausfahrbare Blatt,
die geteilte Ansicht und die Reihe oben, die ich vor einer Stunde noch selbst
gebaut habe. Zusammen konnten sie weniger als das eine Bauteil und mussten
dreimal gepflegt werden. Was jetzt vom System kommt:
- die Seitenleiste aus Glas, mit dem Inhalt **dahinter** statt daneben
- der Knopf in ihrer Kopfzeile, der zwischen Seitenleiste und Leiste **oben**
umschaltet — genau der, den du auf deinen Bildern siehst
- „Bearbeiten" mit Ziehen, Ein- und Ausblenden, für beide Formen gleichzeitig
- auf dem iPhone die gewohnte Leiste unten statt eines Hamburger-Menüs
Die Einstellung „seitlich oder oben" entfällt damit: sie säße neben einem
Systemknopf, der dasselbe besser kann. Was du umstellst, merkt sich das
System selbst; wir geben ihm nur einen Ort zum Aufbewahren.
Die Mediatheken stehen in einem eigenen Abschnitt, der sich in der
Seitenleiste zuklappen lässt. Die Suche bekommt ihre eigene Rolle und wird
deshalb getrennt von den übrigen Einträgen dargestellt.
Angesehen auf iPad (Seitenleiste, Leiste oben, Bearbeiten-Dialog) und iPhone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Dinge aus dem letzten Durchgang.
**Die Startseite hatte zu wenig anzubieten.** Es gab eine Reihe „Zuletzt
hinzugefügt" für alles zusammen. Jetzt gibt es eine je Mediathek — „Neu in
Filme" und „Neu in Serien" sind zwei verschiedene Fragen, und wer nach einem
Film sucht, blättert in einer gemeinsamen Reihe an Folgen vorbei. Dazu
Favoriten, die es bei Jellyfin ohnehin gibt. Die drei Abfragen laufen
nebeneinander; nur die Mediatheken werden streng geholt, weil sie die Probe
darauf sind, ob der Server überhaupt antwortet.
**Die Seitenleiste ist jetzt echtes Glas**, nicht das alte Material: im
ausfahrbaren Blatt über `glassEffect`, in der geteilten Ansicht auf dem iPad
vom System selbst — dort lag mein eigener Hintergrund darüber und machte ihn
stumpf. Die zum Inhalt zeigende Kante ist gerundet, damit das Blatt sichtbar
über der Seite liegt statt daneben zu stehen.
**Die Navigation kann nach oben.** Eine Einstellung unter Erscheinungsbild,
seitlich oder oben. Oben stehen alle Einträge gleichzeitig da, als Glaskapseln
in einer waagerecht scrollenden Reihe — die Zahl der Mediatheken kommt vom
Server, und zusammengequetschte Beschriftungen wären die schlechtere Antwort
als eine Reihe, die weiterläuft.
Angesehen auf iPhone und iPad: beide Formen, das Glas in beiden Leisten, die
neuen Reihen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Kachel ließ sich nicht mehr öffnen, und das war kein Zufall: das
Antippen zeigte erst die Bedienelemente, und die zweite Berührung sollte
vergrößern — traf dabei aber den Abspielknopf, der genau in der Mitte lag.
Jede weitere Berührung landete wieder dort. Vergrößern war damit überhaupt
nicht möglich.
Jetzt gehört die Mitte dem Vergrößern, und zwar beim ersten Antippen. Die
zwei Knöpfe — Pause und Schließen — sitzen dauerhaft sichtbar in den oberen
Ecken, auf einer dunklen Scheibe, damit sie auf hellen Filmbildern nicht
verschwinden. Unten bleibt der Fortschrittsbalken.
Dass die Kachel „einfach weg" war, konnte ich nicht nachstellen; der eine
Fall, den ich reproduziert habe, war mein eigener Attrappenserver mit einer
Fortsetzungsstelle jenseits des Dateiendes. Zwei Dinge machen es trotzdem
unwahrscheinlicher: die Knöpfe sind auch dann zu sehen, wenn das Bild
schwarz bliebe — vorher war eine schwarze Kachel in einer fast schwarzen
Oberfläche praktisch unsichtbar —, und die Bildfläche hängt jetzt in einer
eigenen Hülle je Ansicht. Beim Wechsel zwischen Kachel und Vollbild gibt es
beide kurz gleichzeitig, und eine `UIView` hat nur einen Vorgesetzten: wer
sie zuletzt bekommt, gewinnt. Gab SwiftUI sie unmittelbar heraus, konnte das
Abräumen der einen der anderen die Bildfläche wegnehmen.
Dazu etwas, das mir beim Ausprobieren selbst passiert ist: der Aufmacher
wanderte weiter, während ich nach „Abspielen" griff, und ich startete den
falschen Film. Er hört jetzt bei der ersten Berührung auf zu wandern. Wer
die Hand darauf hat, fährt selbst.
Nachgesehen auf iPhone und iPad: verkleinern, antippen, wieder im Vollbild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bis hierher konnte man sich bei Jellyfin anmelden und stöbern, aber nichts
abspielen. Der Player war die letzte große Stelle, die Plex unmittelbar
kannte.
Drei Mitglieder kommen dafür in den Vertrag: eine Wiedergabe vorbereiten,
sie beenden, den Fortschritt melden. Die Entscheidung **ob** umgerechnet
wird, fällt weiterhin vorher im `PlaybackDecider` — der Server führt sie nur
aus. Genau diese Trennung fehlte: der Player hat Plex' Transcoder
angesprochen und konnte deshalb mit nichts anderem reden.
`MediaBackendError` bekommt einen siebten Fall: `refused(reason:)`. Der
Server hat verstanden und sagt nein, in eigenen Worten. Bei Plex war genau
dieser Satz die einzige brauchbare Spur, und er ging tagelang verloren, weil
es dafür keinen Fall gab.
Bei Jellyfin kommt die Adresse einer umgerechneten Fassung vom Server selbst
(`PlaybackInfo` → `TranscodingUrl`), nicht von uns. Er weiß, welche Spuren er
kopieren kann und welche er neu rechnen muss.
Drei Dinge, die beim Hinsehen auffielen:
- **`startTimeTicks` gehört nicht in die Anfrage.** Sonst entscheiden zwei
Stellen über den Startpunkt: der Server ließe den Strom dort beginnen, und
der Player spränge noch einmal um dieselbe Strecke vor. Bei einundzwanzig
Minuten Fortschritt landet man bei zweiundvierzig.
- **Der erste Bericht braucht einen eigenen Endpunkt.** Ohne ihn erscheint
der Titel bei Jellyfin nirgends als „läuft gerade". Er fiel zunächst aus,
weil er die Laufzeit verlangte — und die steht erst Sekunden nach dem
Öffnen fest. Jetzt tut es für die erste Meldung die des Servers.
- **In der Download-Adresse standen Titel und Fassung vertauscht.** Bei einem
Film mit einer Fassung sind beide Kennungen dieselbe und nichts fällt auf;
bei zweien schon. `MediaFile` sagt jetzt, zu welchem Titel sie gehört.
Live-TV bleibt bei Plex und wird als Einziges noch unmittelbar durchgereicht.
Ein Tuner ist ein Stück Hardware, und die fünfhundert Zeilen, die es gekostet
hat, ihn zu bändigen, gehören in keinen gemeinsamen Vertrag.
Nachgesehen, nicht nur gebaut: gegen die Attrappe läuft ein echtes Video, der
Beginn wird gemeldet, der Fortschritt kommt in Ticks mit Sitzung und Fassung
an. Was die Attrappe **nicht** beantworten kann, ist, ob ein echter Jellyfin
das Geräteprofil annimmt und eine Umrechnung anbietet — das entscheidet erst
ein richtiger Server.
257 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Dinge aus dem ersten Blick auf einen echten Jellyfin-Server.
**„Zuletzt hinzugefügt" waren nicht die Neuzugänge**, sondern eine
alphabetische Liste. Die Reihe kommt jetzt von `/Users/{id}/Items/Latest` —
dem Endpunkt, den Jellyfin dafür hat. Das behebt zwei Fehler auf einmal: die
Sortierung nach Anlegedatum kam alphabetisch zurück, und selbst wenn sie
gegriffen hätte, wäre sie falsch gewesen. Das Anlegedatum einer Serie ist der
Tag, an dem sie in die Mediathek kam; eine Serie, die seit zwei Jahren dort
steht und gestern eine neue Folge bekommen hat, wäre nie aufgetaucht. Die
Antwort ist dort ein nacktes Feld ohne `Items`-Hülle — dafür gibt es einen
Test, sonst bliebe die Startseite eines Tages stumm leer.
**Der Aufmacher war einer und blieb es.** Jetzt sind es bis zu fünf, die alle
sieben Sekunden weiterwandern und sich auch von Hand durchblättern lassen.
Der Zeitgeber hängt an der aktuellen Seite und fängt bei jedem Wechsel von
vorn an — sonst rutscht der Aufmacher genau dann weiter, wenn man sich einen
Titel ansieht. Wer Bewegung abgestellt hat, blättert selbst. Titel ohne
Hintergrundbild werden übersprungen, solange es andere gibt: eine graue
Fläche mit Text ist keine Bühne.
**Die Seitenleiste ist durchscheinend** statt undurchsichtig, und der
Schleier darunter dunkelt schwächer ab — ein voller Schleier hätte aus dem
Milchglas wieder eine Wand gemacht.
**Der Suchlauf ist aus der Mediathek verschwunden.** Er stand im
Dreipunktmenü, einen Tipp neben „Filter" und „Ansicht wechseln", und
beschäftigt den Server minutenlang. Er steht jetzt in den Einstellungen neben
der Mediathek, zu der er gehört.
Dazu eine Jellyfin-Attrappe unter Tools/: ein Server, der gerade genug kann,
dass sich die Oberfläche ansehen lässt. Damit ist diese Runde tatsächlich
angeschaut worden — Anmeldung per Code, Startseite, wandernder Aufmacher,
Seitenleiste, Einstellungen — ohne einen echten Server anzufassen.
Was weiter aussteht: die Seitenleiste oben statt seitlich, und ihre Einträge
selbst zusammenzustellen. Das ist die nächste Runde, nicht diese.
250 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Anmeldebildschirm kannte nur Plex. JellyfinKit war fertig, aber von der
App aus nicht erreichbar — „ich sehe nix von Jellyfin".
Quick Connect steht vorn: der Server nennt einen sechsstelligen Code, den man
in einem bereits angemeldeten Client bestätigt. Das Passwort erreicht Flixy
dabei nie, dieselbe Zusage wie bei Plex. Benutzername und Passwort sind der
Rückfall für Server, auf denen Quick Connect abgeschaltet ist; das Passwort
wird dann für die Dauer einer Anfrage gehalten und danach weggeworfen.
Die Adresse wird nicht erraten, sondern durchprobiert. „192.168.1.50" kann
alles Mögliche heißen, und die Reihenfolge ist der Kern von JellyfinAddress:
eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein Domänenname fast
immer. Andersherum wartet man bei jedem Server ohne Zertifikat erst auf eine
Zeitüberschreitung.
Die Sitzung hält von hier an einen Server hinter dem neutralen Vertrag, nicht
mehr einen Plex-Server. Die Ansichten haben das nicht gemerkt — genau dafür
war MediaKit da. Dabei fällt ab, was Jellyfin kann und Plex nicht: Favoriten
stehen im Aktionsmenü, sobald der Server sie kennt, Sterne nur dort, wo es
sie gibt.
Was noch nicht geht und deshalb auch nicht behauptet wird: Wiedergabe und
Live-TV sprechen weiter unmittelbar mit Plex. Bei Jellyfin verschwindet der
Live-TV-Eintrag aus der Seitenleiste, und der Player sagt, dass die
Wiedergabe der nächste Schritt ist, statt „Server nicht erreichbar" zu
melden — er ist ja erreichbar.
Nebenbei zwei Stellen, die der Übersetzer aus Plex hinterlassen hat:
`catch is CancellationError` griff seit den getippten Fehlern nie mehr, weil
eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt; und das
Ersatz-Hintergrundbild einer Folge war toter Code, weil die linke Seite des
`??` gar nicht optional war.
TokenStore wandert nach MediaKit — Tokens sind nichts Plex-Eigenes. Die
Konten sind nach Server getrennt, damit ein späteres Umschalten nicht jedes
Mal eine neue Anmeldung kostet; „Abmelden" räumt trotzdem beide ab.
248 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der zweite Server. Eigenes Target neben PlexKit, nicht darin — die beiden
teilen die neutrale Schicht und sonst nichts.
Die Anmeldung geht zuerst über **Quick Connect**: die App zeigt einen
sechsstelligen Code, den man in einem bereits angemeldeten Client
bestätigt. Damit gilt hier dieselbe Zusage wie bei Plex — das Passwort
erreicht Flixy nie. Benutzername und Passwort bleiben als Rückfall, falls
der Server Quick Connect abgeschaltet hat; dann hält die App das Passwort
für die Dauer einer Anfrage und speichert es nirgends.
Zwei Stellen, an denen sich eine Übersetzung verrechnet, und die deshalb
Tests haben:
Jellyfin zählt Zeiten in Ticks zu hundert Nanosekunden. Eine Null zu viel,
und aus 44 Minuten werden siebeneinhalb Stunden.
Und es meldet die **ungesehenen** Folgen, nicht die gesehenen. Ungeprüft
übernommen stünde bei einer fast fertigen Serie „9 von 24" statt „15 von
24".
Dass die Adressen den Token tragen, steht hier von Anfang an — bei Plex
hat genau das tagelang gekostet, weil AVPlayer keine Kopfzeilen mitschickt.
Zwei Fähigkeiten sind ausdrücklich nicht da: Sterne-Bewertungen kennt
Jellyfin nicht, nur Daumen hoch und runter. Und die Adresse einer
verkleinerten Fassung gibt es erst nach einer Anfrage — sie zu erfinden
hieße, eine Adresse anzubieten, die nicht antwortet.
Die Startseite ist nachsichtig gebaut: fällt „Als Nächstes" aus, bleibt
deshalb nicht die ganze Seite leer.
Die App bietet Jellyfin noch nicht an — das ist der nächste Schritt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.
Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.
`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.
Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.
Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".
Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Protokoll für das, was jeder Server können muss — blättern, Bilder
liefern, Zustand melden. Bewusst schmal: alles, was nur einer kann,
steht nicht darin, sondern als abfragbare Fähigkeit. Ein gemeinsamer
Nenner, der alles umfasst, wäre keiner mehr.
Deshalb `BackendCapability`. Plex kennt keine Favoriten, Jellyfin keine
Plex-Empfänger — und eine Oberfläche, die Knöpfe für Dinge zeigt, die
der Server nicht kann, ist schlimmer als eine, die sie weglässt. Plex
meldet hier ausdrücklich keine Favoriten und wirft, wenn jemand es
trotzdem versucht; stillschweigend nichts zu tun wäre die schlechtere
Antwort.
`MediaQuery` ersetzt `LibraryQuery` nach oben hin. Deren Sortierwerte
waren Plex' Drahtformat („titleSort:asc"), was hieß: entweder lernt
Jellyfin Plex' Schreibweise, oder jede Ansicht muss wissen, mit wem sie
spricht.
`PlexBackend` ist eine Hülle, keine Umschreibung — die Endpunkte bleiben,
wo sie sind. Der Fehlerübersetzer nimmt den ungetypten Wurf, weil Swift
getippte Fehler durch eine Closure hindurch nicht ableiten kann; der
zweite `catch` ist dabei keine Zierde, sondern verhindert, dass ein
unerwarteter Fehler durchfällt und die Signatur zur Lüge macht.
Die App benutzt noch nichts davon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`MediaItem` und die Typen darunter — die Form, in der die Oberfläche
Titel sieht, unabhängig davon, welcher Server sie geliefert hat.
Die Namen sind dort geblieben, wo sie ohnehin neutral waren: `title`,
`summary`, `year`, `index`. Umbenannt wurde, was Plex-Jargon ist.
`ratingKey` heißt `id`, `viewOffset` heißt `resumeMilliseconds`, und aus
dem „Großelternteil" ist die Serie geworden, die von Anfang an gemeint
war — ein Name, den man in jeder Ansicht im Kopf übersetzen musste.
Zwei Dinge, die dabei ehrlicher werden:
`isFavourite` gibt es, aber Plex meldet immer `false`. Ein Feld, das ein
Server nicht füllt, ist besser als eine Fähigkeit, die es nur auf einem
gibt und die sich nirgends setzen ließe.
Dateigrößen sind jetzt `Int64`. Ein Remux überschreitet vier Gigabyte
mühelos.
Die Übersetzung steht in **einer** Datei und geht in **eine** Richtung.
Die Drahtmodelle bleiben, wo sie sind — sie tragen Eigenheiten, die nur
Plex hat, und die gehören nicht nach oben.
Geprüft wird gegen echte Server-Antworten, nicht gegen erfundene Werte:
genau dort stehen die Eigenheiten, an denen eine Übersetzung scheitert.
Ein Test vergleicht außerdem beide Wege zum Wiedergabe-Profil — sagten
sie Unterschiedliches, spielte ein Titel nach dem Umbau plötzlich über
einen anderen Player.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erster Schritt zu Jellyfin, und er besteht vor allem aus Wegnehmen.
`PlaybackDecider` nahm bisher Plex-Modelle entgegen und las daraus sechs
Felder. Das band die Entscheidung an einen bestimmten Server, obwohl an
der Frage „kann iOS das öffnen?" nichts serverspezifisch ist. Jetzt nimmt
er ein `MediaProfile` — Container, Codecs, Bitrate, ob die Datei
untersucht ist.
Wie ernst die Kopplung war, zeigte der Player selbst: für die
Offline-Wiedergabe baute er sich **erfundene** Plex-Werte zusammen — eine
Media mit Kennung 0, ein Part mit dem Dateinamen als Schlüssel,
zusammengesetzte Spurlisten —, nur damit die Signatur passte. Für eine
Datei, die längst auf dem Gerät lag und mit Plex nichts mehr zu tun
hatte. Dieser Hack ist weg.
Neu ist das Target `MediaKit`: Typen und Rechnungen, die für jeden Server
gelten, ohne eine einzige Abhängigkeit. Darin `MediaProfile` und die
`Duration`-Brücke, die bisher in PlexKit stand — „weil es die unterste
Schicht ist". Das stimmte, solange es nur einen Server gab.
Damit kennen Wiedergabe und Downloads Plex nicht mehr. Zwei der
Abhängigkeiten waren ohnehin tot; die dritte trug nur diese Brücke.
Die wichtigste Testtabelle des Projekts ist mitgewandert und dabei
kürzer geworden: dort steht jetzt, worauf es ankommt, statt vollständiger
Modelle mit Kennungen und Dateipfaden, von denen niemand etwas las.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein frisch angeforderter Stream beginnt vorn, puffert und springt erst
danach an die Stelle, an der man war. Zehn Sekunden lang lief sichtbar
der Filmanfang — und der Eindruck, die Stelle sei weg und man müsse sie
wiederfinden.
Der Umstieg wird jetzt abgedeckt, solange der Player noch nicht dort ist,
und die Abdeckung nennt die Stelle ausdrücklich. Genau darum geht es:
nicht, dass es schneller wird, sondern dass niemand denkt, er habe etwas
verloren.
Aufgehoben wird sie, sobald der Player innerhalb von zwei Sekunden um die
Stelle liegt. Dazu eine Notbremse nach dreißig Sekunden — springt er nie
dorthin, bliebe sonst für immer eine schwarze Fläche über dem Bild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`select(quality:)` stieg aus, wenn die Stufe bereits gewählt war. Nach
einem Fehlschlag stand sie aber schon fest — ein erneutes Antippen galt
als „keine Änderung", und es passierte nichts mehr. Man kam nur wieder
heraus, indem man eine andere Stufe nahm.
Steht die Wiedergabe auf „gescheitert", ist ein erneuter Versuch jetzt
erlaubt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>