Commit Graph
43 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 6b927c21b0 VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er
nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und
Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an
einer Stelle, `PlaybackDecider.fallback`.

Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket.

**Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt,
sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind`
fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy`
kennt kein `allowsFallbackEngine`.

**Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am
Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern,
ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer
nicht an. Statt der zweiten Engine rechnet der Server jetzt neu
(`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer
schwarzen Wand mit einer Fehlernummer.

**Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der
einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird,
kommt schon in einer Hülle, die der Systemplayer öffnet.

Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten:
Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten
auf dem Apple TV, wo es VLC ohnehin nie gab.

Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist
geblieben, sein Ziel nicht.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:04:23 +02:00
ScarriffleandClaude Opus 5 b1973c6890 Bild-in-Bild gab es nur beim Weggehen — jetzt auch auf Zuruf
Du hast keinen Knopf gefunden, weil es keinen gab: `AVPictureInPictureController`
stand da, aber nur mit `canStartPictureInPictureAutomaticallyFromInline`. Das
greift, wenn man die App verlässt — und auf dem Mac gar nicht.

Der Knopf erscheint nur, wenn es wirklich ginge. Das ist nicht immer so:
`attachSurface` steigt für alles ausser `AVPlayerEngine` sofort aus, und
`AVPictureInPictureController` braucht eine `AVPlayerLayer`. Läuft ein Titel
über VLC, gibt es kein Bild-in-Bild — nur Ton im Hintergrund. Genau das war
deine Beobachtung.

**Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst fest,
wenn der Player ein Bild hat; wer beim Zeichnen fragt, bekommt `false` und
zeichnet den Knopf nie wieder. Also meldet es sich über KVO — vom
Input-Thread, deshalb der Sprung auf den Hauptakteur, wie bei den übrigen
Beobachtungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 07:54:46 +02:00
ScarriffleandClaude Opus 5 3f680c7426 Gesehen und Favorit direkt an der Kachel — und die Pinch-Geste greift wieder
Drei Dinge aus dem Gebrauch.

**Das Kontextmenü einer Kachel trug genau einen Eintrag.** „Abspielen",
sonst nichts; der Kommentar daneben nannte es seit Längerem den
natürlichen Ort für „Als gesehen markieren". Jetzt steht es dort —
zusammen mit Favorit und Sternen, soweit der Server sie kennt. Es sind
dieselben Einträge wie im Menü der Detailseite: `ItemActionEntries` wird
von beiden benutzt, statt sie zweimal zu beschreiben.

Zwei Dinge daran sind nicht offensichtlich. Der Inhalt eines
Kontextmenüs lebt nur, solange das Menü offen ist — ein `@State` darin
könnte nichts anzeigen, was nach dem Tippen passiert. Ausgeführt wird
deshalb von der Kachel, die länger lebt, und dort hängt auch die
Fehlermeldung. Und danach muss die Seite neu laden: eine abgehakte Folge
gehört nicht mehr in „Weiterschauen", und der Fortschrittsbalken stimmte
sonst nicht mehr. Dafür gibt es `flixyItemChanged` in der Umgebung —
sonst müsste der Rückweg durch jede Zwischenebene gereicht werden.

**Die Pinch-Geste griff fast nie.** Sie hing an der Bildfläche, und über
der liegen die Sprungflächen — und sobald man einmal getippt hat, die
ganze Bedienung. Eine Zwei-Finger-Geste bildet sich aber nur, wenn
**beide** Finger auf der Ansicht landen, die sie trägt; in dem schmalen
Streifen dazwischen gelang das kaum. Sie hängt jetzt als
`simultaneousGesture` am ganzen Player: sie erkennt mit, statt
dazwischenzugehen, und Tippen, Doppeltippen und Scrubber bleiben
unberührt.

Gemessen im Simulator, Querformat auf einem Pro Max, mit einer echten
Zwei-Finger-Geste:

    auseinander   956 × 440 pt, Rand 0,0 / 0,0
    zusammen      782 × 440 pt, Rand 87,0 / 87,0

**Der Drehknopf zeigte ein Telefon.** Das las sich als Zustandsanzeige —
„so liegt es" — statt als Knopf. Zwei Pfeile im Kreis sagen, was
passiert.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 08:19:19 +02:00
ScarriffleandClaude Opus 5 46b0117984 Füllend heisst füllend — der schwarze Streifen unten ist weg
Im Füllmodus wurde die Bildebene unten um den Sicherheitsrand eingerückt,
damit die Untertitel nicht unter die Bildschirmkante geraten. Das stand
dort unbedingt — auch wenn gar kein Untertitel lief. Übrig blieb dann ein
schwarzer Streifen unter einem Bild, das gerade füllen soll: genau das
Gegenteil dessen, wofür der Knopf da ist.

Der Einzug hängt jetzt daran, ob überhaupt eine Untertitelspur gewählt
ist. Ohne Untertitel reicht das Bild bis an alle vier Kanten, mit
Untertiteln bleibt die Schrift gerettet.

Gemessen auf einem Pro Max, füllend, ohne Untertitel:

    hochkant    440 × 956 pt   Rand 0,0 / 0,0 / 0,0 / 0,0
    quer        956 × 440 pt   Rand 0,0 / 0,0 / 0,0 / 0,0

`PlayerModel.subtitleBottomInset` fällt weg. Die Eigenschaft beschrieb
diese Rechnung, las sie aber niemand — die Ansicht rechnete selbst. Eine
Beschreibung, die nichts beschreibt, zeigt beim nächsten Mal in die
falsche Richtung.

**Nicht geprüft:** wie es mit eingeschalteten Untertiteln aussieht. Die
Attrappe liefert keine Untertitelspur, und ohne eine lässt sich der Fall
im Simulator nicht herstellen. Der Weg ist unverändert geblieben, aber
belegt ist er damit nicht.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 07:55:50 +02:00
ScarriffleandClaude Opus 5 3ed682dd00 Der Sicherheitsrand am Bild war falsch — er schützte vor nichts
Eingepasst bekam das Bild ein Polster in Grösse des Sicherheitsrandes,
damit es nicht unter die Dynamic Island läuft. Nachgerechnet hält das
nicht.

Querformat auf einem Pro Max: 956 × 440 Punkt. Ein Bild in 16∶9 wird bei
voller Höhe 782 Punkt breit — es bleiben 87 Punkte Balken auf jeder
Seite, und die Island braucht 62. Das Bild kommt ihr von selbst nicht
nahe. Das Polster hat also nichts abgewehrt und dabei vierzig Punkte
Höhe gekostet, also jedes zehnte Bildpunktband:

    vorher   695,7 × 400 pt   Balken 130,0 / 130,3
    jetzt    765,3 × 440 pt   Balken  95,3 /  95,3

Beides über VLC gemessen, dieselbe Datei, dieselbe Lage.

Erst was breiter ist als der Schirm (2,17∶1) läuft an die Kanten — bei
21∶9 über die ganze Breite, und dann liegt ein Streifen hinter der
Island. Dafür ist der Zoomknopf da. Ein Film, der nie an den Rand darf,
wäre der schlechtere Tausch: er kostet bei *jeder* Datei Höhe, um bei
wenigen einen Streifen zu retten.

`EdgeInsets.symmetrical` fällt damit weg, samt seiner fünf Tests. Es
hatte genau eine Aufgabe — ein unsymmetrisches Polster mittig zu halten
—, und das Polster gibt es nicht mehr. Eine geprüfte Funktion, die
niemand ruft, ist trotzdem nur Ballast.

iOS und tvOS bauen, 460 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 07:33:18 +02:00
ScarriffleandClaude Opus 5 b0db4f8f5f Bei VLC sass das Bild nie richtig — und der Knopf wirkte nie
Zwei Fehler, die dieselbe Ursache haben: alles, was über VLC läuft, ging
an der Bildebene vorbei, an der die App gedreht hat.

**Das Bild sass nicht mittig.** Nicht ein paar Punkte daneben — gemessen
eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von 711×400,
unten rechts angeschlagen, im falschen Seitenverhältnis. Der Grund steht
im Vertrag von `VLCDrawable`: VLCKit fragt die Grösse **einmal** beim
Anlegen ab und überlässt seine Anzeigefläche danach der
Autoresizing-Maske. Eine Maske hält aber den Unterschied fest, nicht das
Verhältnis — was beim Anlegen zu gross war, bleibt für immer um denselben
Betrag zu gross. Und angelegt wird sie mitten in der Drehung ins
Querformat, während SwiftUI sie einblendet: also regelmässig falsch.

Die Zeichenfläche ist jetzt eine eigene Klasse, die ihrer Anzeigefläche
bei jedem Layout wieder die eigene Grösse gibt. Danach gemessen: 695,7 ×
400 Punkt, Rand links 130,0, rechts 130,3 — mittig.

**Der Knopf für einpassen/füllen tat nichts.** Er schrieb `videoGravity`
auf eine `AVPlayerLayer`, die bei VLC gar nicht im Baum hängt. Die
Einstellung geht deshalb jetzt durch die Engine: `setVideoFit` am
Protokoll, beim Systemplayer eine Voreinstellung, die nichts tut — dort
macht es die Bildebene —, bei VLC `videoFitMode`. `smaller` heisst bei
libvlc „fit inside", `larger` „fit outside".

Gemessen, im Querformat auf einem Pro Max, über VLC:

    eingepasst   695,7 × 400 pt   Rand l/r 130,0/130,3   o/u 20,0/20,0
    füllend      935,3 × 440 pt   Rand l/r  10,3/ 10,3   o/u  0,0/ 0,0

**Zurückgenommen:** im Kommentar an `videoFit` stand, ein `didSet` auf
einer `@Observable`-Klasse werde nicht gemeldet. Das stimmt nicht —
`HomeLayoutSettings` lebt davon. Ein Kommentar, der einen falschen
Mechanismus behauptet, schickt den nächsten in die falsche Richtung;
jetzt steht der belegte Grund dort.

Dazu, damit dieser Weg überhaupt prüfbar ist: die Attrappe meldet mit
`--vlc` DTS-Ton. AVFoundation kann das nicht, also fällt die App auf
VLCKit zurück. Die Beispieldatei bleibt dieselbe — geprüft wird der Weg,
nicht der Ton. Ohne diesen Schalter ist der VLC-Zweig im Simulator gar
nicht zu erreichen, und genau deshalb lagen beide Fehler so lange da.

Ebenfalls dabei: `EdgeInsets.symmetrical` mit fünf Tests und das Polster
im Player, das damit nicht mehr verschiebt statt einrückt.

iOS und tvOS bauen, 465 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 21:05:57 +02:00
ScarriffleandClaude Opus 5 5f635c699a Die Seitenleiste war zu hoch — und der Player ging deshalb hinaus
Dreimal habe ich an den Rändern geschraubt, und dreimal war es nicht das.
Der Fehler sass in der Navigation und zeigte sich im Player.

**Gemessen statt vermutet.** Auf einem iPhone Pro Max im Querformat:

    Fenster  956×440, Rand 0,62,62,20  →  erwartet 832×420
    Wurzel   832×558,67 @62,−69,33     →  138,67 zu hoch

Der Wurzelbaum ist zu gross für den Vorschlag und wird darin zentriert;
(420 − 558,67) / 2 ergibt exakt −69,33. Der Player liegt als Überlagerung
darin und erbt den Kasten — oben fielen neunundsechzig Punkte heraus,
unten neunundvierzig. Genau dort sitzen Kopfzeile und Zeitangaben.

**Die Ursache:** `FlixySidebar` war eine `VStack` ohne Bildlaufansicht,
mit dem Kommentar „die Einträge sind wenige und passen". Auf einem iPad
und im Hochformat stimmt das. Ein grosses iPhone ist quer
vierhundertvierzig Punkte hoch, und die Leiste braucht mit Kopf, Gruppen,
Trenner und Kontozeile gut fünfhundertdreissig. Eine `VStack` schrumpft
dann nicht — sie überragt, der `HStack` nimmt das Maximum, und alles
darüber wächst mit.

Deshalb war es nur dort zu sehen: nur auf einem grossen iPhone ist die
Anordnung quer `regular`, und nur dann gibt es überhaupt eine
Seitenleiste. Auf einem 17 Pro ist quer `compact` — dort stimmte jede
Messung, und genau deshalb konnte ich es dreimal nicht nachstellen.

Jetzt scrollt die Leiste, wenn sie muss (`basedOnSize`). Wo die Einträge
passen, sieht man keinen Unterschied. Nachgemessen: Wurzel 832×420,
Rahmen des Players 956×440 @0,0 — deckungsgleich mit dem Fenster.

**Mitgenommen:** `statusBarHeight` kam aus `proxy.safeAreaInsets.top` und
ging über `topInset` in einen `safeAreaInset` am selben Baum zurück. Das
Layout lief dadurch in Runden auf die richtige Grösse zu, jede Runde
zweiundsechzig Punkte. Der Wert kommt jetzt aus dem Fenster, das ihn ohne
Rückkopplung kennt — eine Runde statt drei.

`ScreenSafeArea` liegt dafür nicht mehr beim Player, sondern bei den
Modellen: es beantwortet jetzt für zwei Stellen dieselbe Frage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:15:50 +02:00
ScarriffleandClaude Opus 5 7bcef03c25 Eingepasst heisst ganz zu sehen — und der Rahmen wird mitgemessen
**Das Bild bleibt im sicheren Bereich, wenn es eingepasst ist.** Vorher
lief es bis an die Bildschirmkante und damit unter die Dynamic Island —
im Querformat frisst sie eine Ecke des Films. „Einpassen" und „ein Stück
fehlt" schliessen sich aus. Das kostet quer gut hundert Punkte Breite;
wem das zu klein ist, legt mit zwei Fingern auf **füllen** um, wo es
bewusst bis an die Kanten läuft und nur der Untertitel ausgespart bleibt.

**Und die Diagnosezeile misst jetzt den Rahmen mit.** Das ist die Frage,
an der alles hängt, und ich habe sie zweimal übersprungen: ist der Rahmen
grösser als der Bildschirm, liegt es nicht an den Rändern — dann wird der
Player in einen zu grossen Kasten gelegt und mittig darin ausgerichtet,
und oben wie unten fällt gleich viel heraus. Genau so sieht es aus, wenn
die Kopfzeile fehlt **und** die Zeitangaben fehlen, während die Knöpfe in
der Mitte stehen.

Der Verdacht, warum ich es nie nachstellen konnte: die Bildschirmfotos
sind 2796×1290, also ein Pro Max. Der ist im Querformat `regular` und
nimmt damit in `RootView` einen **anderen Zweig** — Seitenleiste statt
Leiste unten, mit `safeAreaInset` und `ignoresSafeArea(.container)`. Mein
Simulator ist ein 17 Pro und läuft quer über den anderen Zweig. Belegt
ist das noch nicht; die Zeile wird es sagen.

Im Simulator steht dort „Rahmen 874×402 @0,0 · Schirm 874×402" — gleich
gross, Ursprung in der Ecke, alles liegt drin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:25:13 +02:00
ScarriffleandClaude Opus 5 c7b901b103 Der Player sagt jetzt, was er über seinen Platz weiss
Zweimal habe ich behauptet, die Ränder seien behoben, und zweimal sassen
die Knöpfe weiter ausserhalb. Im Simulator steht dort „SA 0,62,62,20 ·
Schirm 874×402" und alles liegt drin — nachstellen lässt es sich hier
also nicht. Damit ist Raten der falsche Weg.

**Eine Zeile im Entwicklerbau**, unter dem Schieberegler: die Ränder, wie
sie ankommen, die Bildschirmgrösse daneben und der Skalierungsmodus.
Sitzen die Knöpfe daneben, gibt es genau drei Möglichkeiten — die Ränder
kommen als null an, sie kommen falsch an, oder sie stimmen und der Rahmen
ist grösser als der Schirm. Von aussen sehen alle drei gleich aus. Die
Zeile unterscheidet sie.

**Und eine Härtung, die beim Nachlesen auffiel.** `ScreenSafeArea` suchte
nur die Szene, die `foregroundActive` ist. Genau die gibt es in zwei
Momenten nicht: während einer Drehung und kurz nach dem Zurückholen der
App. Wer dann fragt, bekommt `nil`, daraus werden Ränder von null — und
die bleiben stehen, bis sich die Fläche wieder ändert. Die Knöpfe sitzen
dann dauerhaft im Home-Indikator, obwohl die Rechnung stimmt.

Ein Fenster mit den richtigen Rändern ist in dem Moment trotzdem da, es
ist nur nicht „aktiv". Jetzt der Reihe nach: aktiv, sonst sichtbar, sonst
irgendeines. Dazu wird beim Zurückholen der App nachgefasst — eine
Drehung im Hintergrund ändert die Fläche, ohne dass die Ansicht davon
etwas merkt.

Ob das der Fehler war, weiss ich nicht. Die Zeile wird es sagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:59:04 +02:00
ScarriffleandClaude Opus 5 22f824b000 Die Ränder kommen jetzt von der Stelle, die sie kennt
Mein voriger Versuch war an der Quelle falsch, nicht an der Rechnung.

**Was ich gebaut hatte:** die Sicherheitsränder in `RootView` messen und
über die Umgebung an den Player reichen. Gemessen in einer Ansicht,
angewendet in einer anderen, über eine Drehung hinweg — drei
Gelegenheiten, daneben zu liegen. Im Simulator ging es gut, auf dem
Gerät nicht: dort standen im Querformat links 75 und rechts 28 Punkt
Rand, und der Untertitel wurde von der Bildschirmkante abgeschnitten.

**Warum SwiftUI hier nicht hilft.** Der Sicherheitsrand ist dort kein
Wert, den man erfragt, sondern einer, den man verbraucht: wer ihn
respektiert, sieht null, und wer ihn mit `ignoresSafeArea` überschreitet,
sieht danach ebenfalls null. Beide Auswege habe ich durchgespielt und
beide gemessen. Nimmt man der Ansicht das `ignoresSafeArea`, sitzen die
Knöpfe richtig — aber die Bildfläche verliert es mit, und das Bild ist im
Querformat um die Notch-Breite schmaler. Schiebt man einen
`GeometryReader` dazwischen, passiert dasselbe.

**Das Fenster weiß es immer**, in der richtigen Ausrichtung und in dem
Moment, in dem gefragt wird. `ScreenSafeArea` ist die eine Stelle in
dieser App, die so fragt, und die eine, an der es keinen SwiftUI-Weg
gibt. Nachgeholt wird der Wert bei jeder Änderung der Fläche — das ist
genau der Moment des Drehens.

Nachgemessen im Simulator gegen die Jellyfin-Attrappe: im Querformat
meldet das Fenster 0/62/62/20, und damit stehen Kopfzeile, Schieberegler
und Zeitangaben vollständig im sichtbaren Bereich, während das Bild bis
an die Kanten reicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:32:50 +02:00
ScarriffleandClaude Opus 5 4576eac67c Das Bild füllt den Schirm, und die Schrift bleibt darin
Vier Dinge am iPhone-Player, die zusammengehören, weil sie alle davon
handeln, wo der Bildschirm aufhört.

**Einpassen oder füllen.** `videoGravity` stand fest auf `.resizeAspect`,
und zwar in einer Zeile, die nach jedem Neuaufbau erneut läuft — eine
Wahl hätte dort also jeden Qualitätswechsel nicht überlebt. Jetzt steht
sie im Modell, und die Zeile liest sie. Umgelegt wird mit zwei Fingern
oder über einen Knopf; stufenlos wäre falsch, weil alles zwischen den
zwei Zuständen ein Bild ist, das weder ganz zu sehen ist noch den Schirm
füllt. Auf dem Mac erscheint der Knopf nicht: dort steht der
Vollbildknopf darüber, und zwei Knöpfe, die beide „grösser" bedeuten,
sind eine Zumutung.

**Untertitel im Füllmodus.** Dort reicht das Bild über die untere
Bildschirmkante hinaus, und genau dort zeichnet AVFoundation die
Untertitel. Statt sie anzuschneiden, wird die Bildebene um den
Sicherheitsrand eingerückt — rund zwanzig Punkt Bild gegen eine Zeile,
die man lesen kann. Dafür bekommt die Bildfläche echtes Layout: eine
`autoresizingMask` hält Ränder verhältnismässig, und ein fester Einzug
von zwanzig Punkten wäre im Querformat plötzlich vierzig gewesen.

**Die Bedienleiste kannte ihre Ränder nicht mehr.** Der Player bekommt
`ignoresSafeArea()` von der Wurzel und sieht danach überall null —
Schieberegler und Zeitangaben sassen im Home-Indikator, der Zurück-Pfeil
hinter der Dynamic Island, wo ihn niemand drücken konnte. Gemessen wird
jetzt eine Ebene weiter aussen, wo die Ränder noch gelten, und über die
Umgebung nach innen gereicht. Dieselbe Bauart wie der Akzent. Die Zeile
steht ganz am Ende der Kette: stünde sie vor dem Overlay, umschlösse das
Overlay sie, und ausgerechnet der Player ginge leer aus.

**Gedreht wird einmal, gesperrt wird nicht.** Die Maske blieb auf
`.landscape` stehen — wer im Bett auf der Seite lag, konnte nicht mehr
ins Hochformat drehen. Ein Film beginnt quer, weil er quer ist; danach
gehört die Entscheidung dem Nutzer. Dazu ein Drehknopf für den Fall,
dass die Ausrichtungssperre des Systems an ist, und ein Fehlerbehandler
an `requestGeometryUpdate`: eine abgelehnte Drehung ist dann eine
Einstellung und keine Panne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:05:44 +02:00
ScarriffleandClaude Opus 5 1ff65e823a Ein Vollbildknopf im Player — wenn der Mac ihn annimmt
Fuer eine „fuer iPad angepasste\" App gibt es dafuer keine oeffentliche
Schnittstelle. UIKit kennt `isFullScreen` nur lesend und nur fuer Catalyst;
einen Umschalter gibt es nirgends. Das Fenster gehoert AppKit, und an
AppKit kommt diese App nicht heran.

Was bleibt, ist die Antwortkette: `toggleFullScreen:` ist dieselbe Aktion,
die der gruene Knopf und Strg-Cmd-F ausloesen, und UIKit reicht
unbeantwortete Aktionen an das tragende AppKit-Fenster weiter. Ob es das
tut, ist nirgends zugesagt.

Deshalb wird vorher gefragt statt hinterher gehofft: `target(forAction:)`
sagt, ob die Aktion jemand entgegennimmt, **ohne sie auszuloesen**. Erst
wenn es einen Empfaenger gibt, erscheint der Knopf. Ein Knopf, der nichts
tut, ist schlimmer als keiner — er laesst den Nutzer an sich zweifeln statt
an der Plattform.

Das Symbol zaehlt den Zustand selbst mit, weil er sich nicht abfragen
laesst. Wer mit dem gruenen Knopf umschaltet, bringt es aus dem Tritt; das
Umschalten funktioniert trotzdem, es zeigt nur kurz das falsche Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 06:31:46 +02:00
ScarriffleandClaude Opus 5 0815432efd Untertitel lassen sich jetzt einstellen
Groesse in vier Stufen, Schrift und Hintergrund als fuenf feste Paare —
Weiss, Weiss auf Schwarz, Schwarz auf Weiss, Gelb, Gelb auf Schwarz. Feste
Paare statt zweier Regler, weil unabhaengige Achsen unlesbare Mischungen
zulassen: Schwarz auf durchsichtig ueber einer Nachtszene ist eine
gueltige Einstellung und eine wertlose.

Die Uebersetzung nach AVFoundation ist eine Tabelle aus Schluesseln, bei
der jeder Fehler lautlos ist — ein falscher Schluessel laesst den
Initialisierer `nil` zurueckgeben, und dann gibt es gar keine Regel. Auf
dem Bildschirm sieht „keine Regel\" genauso aus wie „Regel ohne Wirkung\".
Zehn Tests decken das ab, darunter die drei Fallen: Alpha zuerst in der
Farbe, Prozentwert statt Punktgroesse, und der Hintergrund auch fuer
„durchsichtig\" ausdruecklich gesetzt statt weggelassen — weggelassen gilt
sonst, was Jellyfins WebVTT selbst mitbringt, und das ist oft ein
schwarzer Kasten.

Als Protokollmethode, nicht als Feld an `PlaybackSource`: die wird einmal
beim Oeffnen gelesen, und eine Einstellung, die erst beim naechsten Film
wirkt, haelt jeder fuer kaputt. Angewandt wird an zwei Stellen — am
laufenden Gegenstand und am frisch angelegten. Nur das erste zu tun hiesse,
dass die naechste Folge wieder in der Vorgabe erscheint.

VLC erbt die leere Vorgabe. Die Einstellungsseite sagt beides: dass es fuer
den Standardplayer gilt, und dass eingebrannte Bilduntertitel von Blu-rays
sich nicht gestalten lassen, weil es dort keinen Text gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:22:22 +02:00
ScarriffleandClaude Opus 5 f48e018fa3 Der Regler springt sofort, und man sieht, bis wohin geladen ist
Zwei Dinge, die beide dasselbe Missverständnis erzeugt haben.

Der Regler blieb beim Spulen liegen, wo er war, und rückte erst nach, wenn
das erste Bild von der neuen Stelle da war — über das Netz Sekunden später.
Das liest sich als „der Sprung ist nicht angekommen", und man spult noch
einmal. Der Sprung ist aber angekommen, in dem Moment, in dem man ihn
auslöst; alles Weitere ist Warten auf Daten, und dafür gibt es den Kreisel.
Die Stelle steht jetzt sofort. Nachzügler von der alten Stelle werden
verworfen, sonst zöge die letzte Meldung den Regler wieder zurück.

Und bis wohin geladen ist, stand nirgends. Dafür ist der `Slider` gewichen:
er hat genau eine Spur, und die gehört dem Fortschritt. Etwas dahinter zu
zeichnen und auf gleiche Innenabstände zu hoffen ginge so lange gut, bis
Apple sie ändert. Der neue Regler zeichnet drei Schichten aus derselben
Rechnung — Grund, Geladen, Gespielt — und hat einen Griff, den ein Finger
trifft.

Der Vorrat wird dabei abgelegt statt ausgerechnet. Eine berechnete
Eigenschaft wird nur gelesen, wenn ohnehin neu gezeichnet wird, und das
geschieht, weil die Abspielstelle weiterläuft. Im pausierten Zustand läuft
sie nicht — und ausgerechnet da will man zusehen, wie der Vorrat wächst.
Die Schleife, die ihn nachführt, lief bisher ausserdem nur bei
„automatisch": wer eine Stufe von Hand gewählt hatte, hätte nie gesehen,
wie weit geladen ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:28:44 +02:00
ScarriffleandClaude Opus 5 67c679006f Untertitel kommen aus der Datei, und Fortsetzen geht wieder
Im Spurenblatt stand „CC" und sonst nichts — derselbe Fehler wie beim Ton:
das ist AVFoundations Name für den einen Untertitel, den ein umgerechneter
Stream mitbringt. Zur Auswahl steht jetzt, was in der Datei liegt, und die
Nummer geht als `subtitleStreamIndex` mit in die Anfrage.

`-1` heisst dabei ausdrücklich „keine". Das ist nicht dasselbe wie nichts zu
schicken: dann sucht der Server sich einen aus, und es steht plötzlich ein
Untertitel im Bild, den niemand bestellt hat.

Und der Klick ins Bild: Anhalten ging, Fortsetzen nicht. Der Abdunkler
hinter der Bedienung deckt das ganze Fenster ab und lag über dem Film —
solange die Bedienung zu sehen war, landete jeder Klick auf ihm. Auffallen
konnte das nur an einer Stelle: nach dem Pausieren bleibt die Bedienung
stehen, also traf der Klick, der wieder starten sollte, nie das Bild. Der
Abdunkler ist jetzt unantastbar; er dunkelt ab, mehr nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:14:07 +02:00
ScarriffleandClaude Opus 5 60194c2d2a Die Bedienung blieb stehen, weil der Zeiger sich nie bewegt hat
Seit die Bedienung dauerhaft montiert ist, zeichnet der Scrubber darin im
Sekundentakt neu. `onContinuousHover` meldet nicht nur bei Bewegung, sondern
auch bei jedem Neuzeichnen — also setzte die Ausblendfrist sich endlos
selbst zurück. Die Bedienung blieb für immer stehen, und die Überblendung,
die dadurch nie stattfand, sah aus wie eine kaputte Animation. Sie war nie
das Problem; das war sie schon beim letzten Anlauf nicht.

Jetzt gilt der Zeiger nur als bewegt, wenn er an einer anderen Stelle ist
als zuletzt.

Und der Klick in den schwarzen Rand: ich habe zweimal ausgerechnet, wo das
Bild aufhört, und lag zweimal daneben. Jetzt rechnet das Layout. Die
Bildfläche ist auf ihr Seitenverhältnis zugeschnitten und hat ihren eigenen
Griff; der schwarze Rand ist schlicht der Hintergrund dahinter. Was am
Player ankommt, ist damit per Definition daneben gegangen — es muss niemand
mehr feststellen.

Damit fallen `videoRect`, `videoFrame`, `playerSize`, der benannte
Koordinatenraum und die Abfrage der `AVPlayerLayer` ersatzlos weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 18:01:27 +02:00
ScarriffleandClaude Opus 5 596a143545 Die Tonspur wird aus der Datei gewählt, nicht aus dem Stream
Im Spurenblatt stand eine einzige Zeile: „Unbekannt". Das ist der Name, den
AVFoundation einer namenlosen Spur gibt — und bei einem umgerechneten
Stream liefert der Server genau eine. Der Player hatte nichts zur Auswahl,
weil die Wahl längst gefallen war, bevor er gefragt wurde.

Gewählt wird jetzt aus den Spuren, die **in der Datei** stehen, und zwar
bevor der Stream ausgehandelt wird. Die Nummer geht als `audioStreamIndex`
mit hinaus; wer mitten im Film umstellt, bekommt den Stream neu — ausser
bei unveränderter Wiedergabe, wo der Player ohne Unterbrechung umschaltet.

Die Abwägung selbst macht `AudioTrackChooser`: gemerkte Spur, dann
Wunschsprache, dann Rangliste, dann Standardspur — und Audiodeskription
mit deutlichem Abzug statt Ausschluss, damit eine Sprache, die es nur so
gibt, immer noch besser ist als Stille.

Drei Ebenen in `LanguagePreferences`: Serie, Mediathek, allgemein. Gefragt
wird von unten nach oben. Die Mediathek wird nur nachgeschlagen, wenn
überhaupt jemand eine Sprache je Mediathek gesetzt hat — sonst kostete jede
Wiedergabe eine Anfrage für eine Ebene, die niemand benutzt.

Wer von Hand umstellt, dessen Wahl wird für die Serie gemerkt, ohne
Nachfrage. Der Dialog „für diese Serie merken?" ist genau die Art, die man
nach dem dritten Mal wegklickt. Sichtbar ist es trotzdem: das Spurenblatt
zeigt es und lässt es löschen.

Dazu die Rangliste in den Einstellungen, ziehbar, und der Schalter
„Audiodeskription meiden".

Und die Player-Oberfläche blendet endlich als Ganzes über. Die Kopfzeile
hatte gar keine Überblendung — sie sprang herein, während die Bedienung
darunter verblasste. Jetzt steht die Animation an einer Stelle statt an
sechs, und sie läuft über Deckkraft statt über Einfügen und Entfernen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 16:31:38 +02:00
ScarriffleandClaude Opus 5 18958aadbc Der Klick in den schwarzen Rand trifft jetzt den schwarzen Rand
Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund
steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse
ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`.
Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied
zwischen beiden — die Rechnung lag um diesen Betrag daneben.

Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen
stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber
wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst,
wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten
Bild und für VLC, das keine solche Ebene hat.

Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst
wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 06:49:15 +02:00
ScarriffleandClaude Opus 5 10caa5fdd1 Platzhalter statt Ratespiel, und zwei Griffe im Player
**Das Raster sagt jetzt, ob noch etwas kommt.** Eine Mediathek lädt
seitenweise nach, und wer unten ankam, sah ein Raster, das aufhört — ohne
unterscheiden zu können, ob das alles war oder gerade nachgeladen wird.
Beides sah gleich aus.

Beide Server nennen die Gesamtzahl (Jellyfin `TotalRecordCount`, Plex
`totalSize`), und wir lesen sie längst. Also stehen die nächsten Kacheln
schon da, in der Form, die sie haben werden — gedeckelt auf ein Dutzend,
weil zweihundert Platzhalter niemandem helfen. Ist wirklich alles da,
steht es am Ende ausdrücklich: „Alle 252 Titel geladen".

Gegen die Attrappe mit 252 Titeln durchgeblättert und nachgesehen: die
Zeile steht da, und alle 252 sind angekommen.

**Herein sanft, hinaus zügig.** Das Einblenden der Bedienung begleitet
eine Bewegung des Zeigers und soll wirken, als käme sie der Hand
entgegen; zwei Zehntel wirkten wie ein Aufblitzen. Das Ausblenden
dagegen ist ein Aufräumen, und dabei muss niemand zusehen.

**Ein Klick ins Schwarze blendet um, statt anzuhalten.** Ein Film füllt
das Fenster fast nie ganz aus — die Balken daneben gehören nicht zum
Film, und dort zu klicken ist die natürliche Geste für „zeig mir die
Bedienung". Wo das Bild aufhört, wird aus `presentationSize` gerechnet,
also aus dem, was wirklich ankommt. Fehlen die Maße, verhält es sich wie
bisher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:50:26 +02:00
ScarriffleandClaude Opus 5 279d8e36ae Zwei Fehler um dieselbe Bildfläche
**Die Wiedergabe sprang von selbst wieder an.** Auf dem Mac pausiert ein
Klick ins Bild — und der Klick, mit dem man ein Fenster nach vorn holt,
kommt ebenfalls dort an. Man aktiviert das Fenster und drückt dabei auf
Play. Klicks innerhalb einer Viertelsekunde nach dem Aktivieren zählen
deshalb nicht: länger braucht niemand bis zum ersten gemeinten Klick,
kürzer erwischt den Fall nicht.

**Schwarzbild mit Ton nach dem Vergrössern.** Kachel und Vollbild
bestehen beim Wechsel kurz gleichzeitig, und beide holten sich dieselbe
Bildfläche in ihre Hülle. Eine Ansicht hat aber nur einen Vorgesetzten —
wer zuletzt aktualisiert, gewinnt, und das war nicht verlässlich die
richtige. Blieb die Fläche bei der Kachel, die gleich darauf verschwand,
lief im Vollbild der Ton weiter und das Bild blieb schwarz.

Die Hülle allein genügte nicht: sie verhindert, dass das Abräumen der
einen Ansicht der anderen etwas wegnimmt, entscheidet aber nicht, wer
die Fläche bekommen **soll**. Das sagt jetzt `isActive` — abgeleitet aus
`playback.presentation`, also aus derselben Quelle, die auch bestimmt,
welche Ansicht überhaupt zu sehen ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:44:20 +02:00
ScarriffleandClaude Opus 5 ab597ee2f0 Stillstand ohne Absicht ist kein Pausieren
`timeControlStatus` meldet zwei verschiedene Dinge als `.paused`: das
Anhalten durch den Nutzer und den Fall, dass `play()` gerufen wurde, der
Player aber nicht losläuft — weil die Quelle nicht bereit ist oder weil
nichts ankommt. Die Engine hat beides als „pausiert" weitergegeben.

Sichtbar war das als Player mit Abspieldreieck und Scrubber auf 0:00,
während in Wahrheit auf Daten gewartet wurde. Wer dann tippte, drückte
auf Play bei etwas, das längst laufen sollte.

Die Engine führt jetzt mit, ob überhaupt jemand spielen wollte.
Stillstand mit Absicht ist Pause, Stillstand ohne Absicht ist Warten.

Dazu die Anzeigeregel, wie gewünscht: **der Kreisel gilt immer**, mit
oder ohne eingeblendete Bedienung. Er ist keine Verzierung der Knöpfe,
sondern die Auskunft „es kommt gleich weiter" — und die braucht man
gerade dann, wenn nichts eingeblendet ist und man vor einem stehenden
Bild sitzt. Das Play-Pause-Symbol erscheint dagegen nur mit der übrigen
Bedienung, und an seiner Stelle bleibt beim Laden eine Lücke, damit der
Kreisel hindurchscheint statt zwei Anzeigen übereinanderzulegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:14:10 +02:00
ScarriffleandClaude Opus 5 fc812f5a25 Zwei Schlüssel ohne Platzhalter, zwei Kreisel übereinander
**Die Schlüssel.** Im Quelltext steht `"season.number \(season)"`, der
Schlüssel heißt damit `season.number %lld`. Im Katalog stand er ohne
Platzhalter — also traf er nie, und die App zeigte den Schlüssel selbst:
„The 50 · season.number 1 · episode.number 1". Beide umbenannt. Ein
Überbleibsel `episode.number %@` ohne eine einzige Übersetzung ist
mitgegangen, und aus „Episode" wurde „Folge", wie überall sonst im Haus.

**Die Kreisel.** Die Abdeckung beim Wiederherstellen der Stelle ist eine
ganze Seite, kein Schleier — nur lag das Übrige weiter darüber. Zu sehen
waren Bedienelemente mitten im Satz, ein Kreisel in der Abdeckung und ein
zweiter im Abspielknopf, den man in dem Moment ohnehin nicht drücken
soll. Jetzt ist die Abdeckung ausschließlich. Die Kopfzeile bleibt: sie
ist der Weg zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:52:08 +02:00
ScarriffleandClaude Opus 5 8a63f815cd Das Qualitätsblatt hat die App angehalten
Kein Hänger, sondern ein Absturz, der nicht durchkam. Der Stapel des
eingefrorenen Prozesses endet in `_assertionFailure` beim Lesen eines
`@Environment`-Werts — mitten in `SheetBridge.present`, also während das
Blatt aufgebaut wird. Weil der Player in diesem Moment in einem
Layoutdurchlauf steckt, erschien nie eine Meldung: das Fenster stand
still.

`@Environment(PlaybackSettings.self)` ohne Fragezeichen ist keine Bitte,
sondern eine Behauptung — fehlt der Gegenstand, hält Swift die Anwendung
an. Und ein Blatt hängt an einem eigenen Darstellungsbaum. Auf dem iPad
kam die Umgebung des Players dort bisher an, auf dem Mac nicht.

Zwei Änderungen, beide nötig:

- Der Player reicht `PlaybackSettings` und den Akzent ausdrücklich in
  seine Blätter hinein, statt sich auf Vererbung zu verlassen.
- `QualitySheet` liest die Einstellungen als Optional. Ein Vergessen
  kostet künftig eine fehlende Zeile im Blatt statt der ganzen App.

Dazu ein Test, der genau diesen Weg geht: abspielen, Qualitätsknopf,
Blatt da? Er lief schon vor der Korrektur durch — der Unterschied
zwischen iPad und Mac ist mit einem Simulator nicht zu fassen. Er hält
trotzdem fest, dass der Weg überhaupt gegangen werden kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:45:09 +02:00
ScarriffleandClaude Opus 5 f3ea1ea891 Die Fensterleiste schluckte die halbe Kopfzeile
Der Player deckt das ganze Fenster ab, und auf dem Mac zeichnet das
System seine Ampelknöpfe darüber. Die Kopfzeile lag damit nicht nur
optisch unter ihnen — der Streifen gehört der Fensterleiste, und die
nimmt jeden Klick für sich. Verkleinern, Schließen, Tonspur, Untertitel
und die Qualitätsstufe waren dort tot, und ein Doppelklick auf dieselbe
Stelle legte das Fenster ins Dock.

Jetzt beginnt die Kopfzeile auf dem Mac achtundzwanzig Punkte tiefer.

Dazu: Tabulator springt in der Jellyfin-Anmeldung vom Namen zum Passwort
und zurück. „Name, Tab, Passwort, Eingabe" ist der Weg, den jeder mit
einem Passwortspeicher nimmt; SwiftUI schiebt den Fokus dort nicht von
selbst weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:56:36 +02:00
ScarriffleandClaude Opus 5 37a6c2648f Der Mac ist nicht immer Catalyst
Die Bedienung mit Zeiger und Tastatur hing an
`#if targetEnvironment(macCatalyst)`. Das ist in dem Bau, der tatsächlich
benutzt wird, **falsch**: „für iPad angepasst" ist buchstäblich der
iOS-Bau, der auf Apple Silicon startet, und dort ist die Abfrage nicht
erfüllt. Klick-zum-Anhalten, Leertaste und die Pfeiltasten waren damit
genau dort abgeschaltet, wo sie gebraucht werden.

Jetzt beantwortet `FlixyPlatform.isMac` die Frage zur Laufzeit und kennt
beide Wege. Die Ansicht bleibt dabei eine einzige: `focusable(_:)` nimmt
den Schalter entgegen, die Tastenhandler geben sonst `.ignored` zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:40:43 +02:00
ScarriffleandClaude Opus 5 9a4b6b1e06 Ein Kreisel ist kein Pausenknopf
Während der Player auf Daten wartete, stand im mittleren Knopf das
Abspieldreieck und der Ladekreis lag dahinter. Das las sich als
„angehalten": man drückte auf Abspielen, und es geschah nichts — es lief
ja bereits, es kam nur nichts an.

Jetzt sitzt der Kreisel **im** Knopf, dort wo sonst das Symbol steht. Der
freistehende Kreisel in der Bildmitte bleibt für den Fall, dass die
Bedienelemente ausgeblendet sind. Zwei übereinander gibt es nicht mehr.

`togglePlayPause` zählt das Warten jetzt zum Laufen. Vorher führte ein
Tipp in diesem Moment zu `play()` auf etwas, das schon spielte.

Dazu auf dem Mac, wie im Browser auch:

- Ein Klick ins Bild hält an, statt erst die Bedienelemente zu holen. Die
  kommen dort von allein, sobald sich der Zeiger regt.
- Leertaste hält an, Pfeil links und rechts springen.

Mit dem Finger bleibt es beim Alten: ohne Zeiger gibt es keine Bewegung
ohne Berührung, und ein Tipp, der sofort anhält, nähme den einzigen Weg
zu Scrubber, Tonspur und Qualität.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:33:24 +02:00
ScarriffleandClaude Opus 5 d54954b9c2 macOS: die App läuft jetzt auf dem Mac
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>
2026-09-13 15:45:13 +02:00
ScarriffleandClaude Opus 5 21c5a8b46b Player: vier Knöpfe, die vorher fehlten oder nichts sagten
**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>
2026-09-13 15:36:24 +02:00
ScarriffleandClaude Opus 5 dace5d1543 Einstellungen: sechs Bildschirme statt einer Liste — und was sie steuern
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>
2026-09-13 14:56:08 +02:00
ScarriffleandClaude Opus 5 33dfac352a Player: beim Umstellen sah man den Filmanfang statt seiner Stelle
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>
2026-09-12 22:21:51 +02:00
ScarriffleandClaude Opus 5 b53fce65b8 Player: die Sprungflächen lagen über allen Knöpfen
Ein Überzug liegt in SwiftUI zwangsläufig obenauf. Ich hatte die
Doppeltipp-Flächen als `.overlay` über den ganzen Player gelegt, also
eine durchsichtige Fläche vor jeden Knopf — Pause, Scrubber, Qualität,
Kopfzeile, alles wirkungslos. Im Stapel entscheidet dagegen die
Reihenfolge, und dort liegen sie jetzt: über dem Bild, unter den
Bedienelementen.

Dazu kleiner. Volle Bildschirmhälften decken die Mitte ab, wo der
Pause-Knopf sitzt, und den oberen Rand mit der Kopfzeile — beim Zielen
auf einen Knopf träfe man zuerst die Fläche. Jetzt sind es zwei Streifen
von je dreißig Prozent Breite in der mittleren Höhe, mit Abstand zu oben
und unten.

Tippen außerhalb zeigt und verbirgt die Bedienelemente wie zuvor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:56:18 +02:00
ScarriffleandClaude Opus 5 12bb49615b Player: die Qualitätsleiter des Servers, Bitrate im Blick, Doppeltipp
Die vier erfundenen Stufen weichen der Leiter, die der Server selbst
meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000,
12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine
eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt —
er rundet dann still auf die nächste, und die Anzeige log.

Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p
hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus.

Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene
Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten
Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere,
was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort.

„Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen
reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild
ausgeblendet.

Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die
Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt
sonst das doppelte.

Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder
rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im
Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören.
Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg,
er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben.

Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler.
Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:50:39 +02:00
ScarriffleandClaude Opus 5 c348ab0755 Kachel größer ziehen, Senderlogos vollständig, Sendung im Player
Drei Sachen aus dem Feldtest.

Der Griff zum Größerziehen war wirkungslos, und zwar durch meinen Fehler:
die Drehung lag auf dem ganzen Überzug statt auf dem Symbol. Bei zwei der
vier Ecken kippte damit die gesamte Trefferfläche weg, und man griff ins
Leere. Jetzt sitzt der Griff über `overlay(alignment:)`, nur der Pfeil
dreht sich, und das Ziehen hat Vorrang vor dem Verschieben der Kachel —
sonst gewinnt das Verschieben, das am selben Ort hängt.

Senderlogos sind Schriftzüge und fast nie quadratisch. Formatfüllend in
einem Quadrat fehlten links und rechts genau die Teile, an denen man den
Sender erkennt. Sie bekommen jetzt eine breitere Fläche und werden
eingepasst statt beschnitten — `FlixyArtwork` kann beides, und welches
gilt, entscheidet der Aufrufer.

Und im Player steht bei Live-TV, was gerade läuft. Der Name reist mit dem
Einschalten mit, denn nachzuschlagen ist er nicht — ein Sender hat keine
Metadaten in der Mediathek. Beim Aufdecken der Bedienelemente wird er
nachgeholt, aber nur, wenn die alte Sendung wirklich abgelaufen ist:
sonst stünde derselbe Name den ganzen Abend, und das wäre derselbe
Fehler, der eben erst im Programm behoben wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:19:50 +02:00
ScarriffleandClaude Opus 5 26c909a2e2 Mini-Player: die Wiedergabe überlebt das Zurückgehen
Wer im Fernsehen etwas sieht und nebenher nachsehen will, was sonst
läuft, musste bisher abbrechen. Plex' eigene Handy-Apps können das auch
nicht — kein Grund, es hier genauso zu machen.

Der eigentliche Umbau ist nicht die Kachel, sondern der Besitz. Die
Wiedergabe hing an der Sichtbarkeit der Ansicht, und zwar dreifach: das
Modell war `@State` im Player, `onDisappear` hat den Motor abgeräumt, und
die Bildebene entstand in `makeUIView` neu. Zurückgehen hieß deshalb:
Motor tot, Transcoder aus, beim Live-TV der Tuner freigegeben.

Jetzt gehört das Modell der Wurzelansicht, die immer da ist. Verkleinern
und Vergrößern wechseln nur noch die Darstellung; beendet wird die
Wiedergabe an genau einer Stelle, und zwar weil jemand es will.

Die Bildebene hält das Modell selbst — dasselbe Muster, das VLC seit
jeher verwendet. Dieselbe `UIView` wandert zwischen Kachel und Vollbild,
statt neu zu entstehen; sonst setzte das Bild bei jedem Wechsel neu an.
Bild-in-Bild wandert mit und bekommt endlich einen Delegaten: bisher war
es eine Einbahnstraße, aus der man nicht in die App zurückkam.

Die Kachel schwebt und lässt sich in jede Ecke ziehen, statt als Leiste
unten zu kleben — eine Leiste nähme jeder Liste Platz weg und müsste in
jedem Bildschirm bedacht werden. Die Eckenberechnung steht in
MiniPlayerPlacement und ist geprüft: in einer View wäre eine Kachel, die
halb aus dem Bild ragt, erst auf dem Gerät aufgefallen.

Der Pfeil oben verkleinert, ein neuer Knopf daneben beendet wirklich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 17:09:22 +02:00
GuidoandClaude Opus 5 6150b018ec Vier Punkte: Player-Hänger, kaputte Downloads, Stufenliste, Kopfleiste
Player hing sich auf
  Die Qualitätsauswahl war ein Menu. Schloss man es durch einen Tipp ins
  Bild, blieb eine unsichtbare Ebene zurück, die alle weiteren Tipps
  schluckte — die Bedienelemente kamen nicht mehr, pausieren ging nicht
  mehr. Jetzt ein confirmationDialog, der sich selbst wegräumt. Nach dem
  Schließen werden die Bedienelemente eingeblendet und die Ausblendfrist
  neu gestartet.

Downloads galten als fertig, waren aber leer
  Zwei Fehler übereinander. Erstens gilt ein URLSessionDownloadTask auch
  bei 404 oder 500 als erfolgreich und legt die Fehlerseite als Datei ab.
  Zweitens überschrieb ich danach die erwartete Größe mit der empfangenen
  — womit die Anzeige auf hundert Prozent stand und der Beweis weg war.
  873 MB waren in einer Sekunde „fertig", und beim Abspielen kam nur
  „Wiedergabe wurde unterbrochen".

  Jetzt wird der Statuscode geprüft, die erwartete Größe bleibt stehen,
  und eine unplausible Datei wird verworfen statt verbucht: unter 100 KB
  ist keine Videodatei, unter der Hälfte der angekündigten Größe ist eine
  abgebrochene Übertragung. Sechs Tests dazu.

Stufenliste ergab keinen Sinn
  Für einen 720p-Inhalt standen Original, Full HD und HD mit derselben
  geschätzten Größe untereinander — das sah aus, als wäre die Schätzung
  kaputt. Sie war es nicht, die Stufen waren es: hochrechnen bringt nur
  eine größere Datei. Angeboten wird jetzt nur noch, was unter der
  Auflösung der Datei liegt, und beim Original steht dabei, was es ist:
  „Original · 720p · 873 MB".

Kopfleiste
  Kein Hintergrund mehr statt eines weichgezeichneten — die Knöpfe
  bringen ihre eigene Glasfläche mit. Der Titel ist weg: der Name der App
  gehört auf den Homescreen, nicht über jeden Bildschirm.

145 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:32:19 +02:00
GuidoandClaude Opus 5 ac0c27c026 Fünf Punkte aus dem Praxistest
iPad meldete sich nie an
  Die Ursache ist unangenehm speziell: Wer im Browser schon bei Plex
  angemeldet ist, wird von app.plex.tv/auth sofort auf watch.plex.tv
  weitergeschickt — OHNE dass der Code verknüpft wird. Man sieht sich
  angemeldet, die App wartet vergeblich, und es gibt keinerlei Hinweis.

  Jetzt wird plex.tv/link mit eingetragenem Code geöffnet. Diese Seite
  tut nur eine Sache: den Code bestätigen. Die Gerätekennung steckt
  ohnehin schon im PIN.

  Dazu ein Ausweg: bleibt es nach fünfzehn Sekunden ohne Token, erscheint
  der Code im Anmeldebildschirm zum Abtippen. Schließt man das Fenster
  ohne Erfolg, bleibt er stehen statt abzumelden — er gilt weiter.

Download fragte nicht nach der Qualität
  Jetzt Original, Full HD, HD oder Sparsam, jeweils mit geschätzter
  Größe. Die Größe ist das eigentliche Entscheidungskriterium — „Full HD"
  allein sagt nicht, ob es aufs Telefon passt.
  Die komprimierten Stufen laufen über den Universal-Transcoder und sind
  weiterhin NICHT am Server geprüft; steht so im Quelltext.

Schwarzes Bild ohne Anzeige
  Der Player kannte nur „spielt" und „spielt nicht". Wartete er auf
  Daten, stand man vor einem Standbild ohne Hinweis. Es gibt jetzt einen
  eigenen Pufferzustand mit Anzeige.

Aufmacher war beschnitten
  Er stand auf 3:4, Plex liefert als `art` aber 16:9 — mit scaledToFill
  ging links und rechts je ein Viertel verloren, ohne dass man es dem
  Bild ansah. Die Seitenverhältnisse liegen jetzt an einer Stelle in
  FlixyAspectRatio.

Schwarze Leiste über dem Aufmacher
  .ultraThinMaterial statt undurchsichtigem Grund: das Bild läuft
  unscharf hinter der Leiste weiter, statt an einer harten Kante zu
  enden. Weichgezeichnet, nicht abgedunkelt.

139 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:06:51 +02:00
GuidoandClaude Opus 5 bf0a63a924 Vier Punkte aus dem Praxistest
Download-Knopf tat nichts
  DownloadService.plan gab still nil zurück, wenn es keinen Auftrag
  bauen konnte, und download() reichte das als nil weiter. Der Knopf sah
  aus, als wäre er kaputt, dabei war der Grund jedes Mal bekannt. Es
  gibt jetzt einen eigenen Ablehnungsgrund mit vier unterschiedlichen
  Begründungen — keine Verbindung, keine Datei, kein abrufbarer Teil,
  keine Adresse. Eine Funktion, die kommentarlos nichts tut, ist selbst
  der Fehler.

Live-TV galt als „nicht eingerichtet"
  Plex legt die Empfängerliste unter dem Schlüssel `Dvr` ab, die Hülle
  kannte aber nur Metadata, Directory und Hub. Sie las eine leere Liste
  und zog daraus die falsche Schlussfolgerung — auf einem Server, auf
  dem ein Empfänger eingerichtet war. Dasselbe galt für die
  Anbieterliste unter `MediaProvider`, ohne die es keinen
  Programmführer gibt.

  Beim Umbau wäre beinahe die Fehlerdiagnose verloren gegangen: mit
  `try?` über alle Schlüssel verschwände auch ein echter Dekodierfehler
  und ergäbe stillschweigend eine leere Liste. Aus „ein Feld heißt jetzt
  anders" würde „die Mediathek ist leer". Jetzt wird erst geprüft, ob
  der Schlüssel da ist, und dann ohne try? dekodiert. Fünf Tests halten
  beides fest.

Qualität im Player wählbar
  Automatisch, Original, 1080p, 720p, 480p. Der Eingriff wird genau dann
  gebraucht, wenn es ruckelt — dafür will niemand den Film verlassen.
  Ein Wechsel baut den Strom neu auf (die Stufe steckt in der Adresse),
  merkt sich die Stelle und fährt sie wieder an. Der alte Transcoder
  wird vorher angehalten, sonst rechnet der Server zwei Ströme für einen
  Zuschauer.

AVPlayer-Anzeige entfernt
  Sie war eine Prüfhilfe aus Phase 1. Dieselbe Angabe steht weiterhin
  unter Einstellungen → Diagnose, wo sie hingehört.

137 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:44:20 +02:00
GuidoandClaude Opus 5 f10f0c4086 Diagnose-Bildschirm und Build-Datum
Zweimal in Folge stand „es läuft nicht" gegen „bei mir ist es behoben",
ohne dass sich klären ließ, welcher Stand überhaupt auf dem Gerät liegt.
Das soll nicht noch einmal passieren.

Einstellungen → Diagnose zeigt jetzt:
- welcher Server und über welche Verbindung, mit Warnung bei der
  gedrosselten Plex-Vermittlung
- den letzten Abspielversuch: Titel, gewählter Weg, Quelladresse
- jeden Schritt mit Zeitabstand zum Start. Der Abstand ist die
  eigentliche Auskunft: liegt zwischen „Quelle gewählt" und „spielt"
  eine Minute, hängt es an der Leitung; kommt „spielt" gar nicht, am
  Motor.
- den Zeitpunkt, zu dem der laufende Build entstanden ist

Das Build-Datum kommt aus dem Änderungsdatum der eigenen Binärdatei —
kein Build-Skript, keine erzeugte Datei, und immer richtig.

Der Bericht lässt sich kopieren und enthält keinen Token: die
Quelladresse wird ohne Abfrageteil abgelegt, und dort steckt er.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 20:56:46 +02:00
GuidoandClaude Opus 5 222ac1c968 Vier Fehler aus dem ersten echten Gebrauch
Kein Video lief jemals an
  AVPlayerEngine.load() wartete auf asset.load(.duration) und die
  Spurenliste, BEVOR es zurückkehrte — und erst danach rief das Modell
  play(). AVFoundation muss für die Dauer aber so viel von der Datei
  holen, bis es den Index findet, und der sitzt bei MP4 wie MKV oft am
  Ende. Über eine langsame Leitung sind das Minuten, in denen nur ein
  Ladekreis zu sehen war, obwohl der Player längst hätte anfangen können.
  Jetzt kehrt load() zurück, sobald das Item hängt; Dauer und Spuren
  kommen nach und melden sich über den Ereignisstrom.
  Dazu AVURLAssetPreferPreciseDurationAndTimingKey: false — sonst
  durchsucht AVFoundation bei manchen Formaten die ganze Datei.

  Zweiter Fund an derselben Stelle: VLCMedia(url:) ist nullable. Ging es
  schief, blieb media still nil, play() tat nichts und es gab keinen
  Fehler — nur einen Ladekreis ohne Ende. Wird jetzt gemeldet.

Kein Querformat im Player
  requestGeometryUpdate allein dreht nicht: das System dreht sofort
  zurück, solange die erlaubten Ausrichtungen es nicht hergeben. Die
  Erlaubnis kommt nur über den App-Delegaten, deshalb OrientationLock
  als Brücke. Player dreht ins Querformat, beim Verlassen zurück. iPad
  bleibt frei — dort ist Querformat beim Blättern der Normalfall.

Uhrzeit über dem Film
  Die Statusleiste war nur ausgeblendet, solange die Bedienelemente weg
  waren. Jetzt immer.

Sehr langer Start
  Zwei Ursachen. Erstens legte der Initialisierer der App die
  Hintergrund-URLSession an — die spricht über XPC mit nsurlsessiond und
  verzögerte den allerersten Bildaufbau. Jetzt erst bei Bedarf, und wer
  keine Downloads hat, braucht sie nie. Zweitens fragte jeder Start erst
  plex.tv nach der Serverliste und suchte die Verbindung neu. Die zuletzt
  funktionierende wird jetzt gemerkt (Server-Token im Schlüsselbund) und
  mit kurzer Zeitgrenze zuerst probiert; die vollständige Suche greift
  nur noch, wenn sie nicht mehr antwortet — etwa wenn man von zuhause
  weggeht.

Dazu ein Hinweis im Player, wenn die Verbindung über die gedrosselte
Plex-Vermittlung läuft. Sonst sucht man den Fehler in der App.

132 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 20:43:26 +02:00
GuidoandClaude Opus 5 62158303a2 Phase 3: Download-Engine und Offline-Wiedergabe
Engine
- Hintergrund-URLSession: läuft weiter, wenn die App zu ist. Der ganze
  Zustand liegt in SwiftData, weil es nach einem Neustart kein anderes
  Gedächtnis gibt.
- Pausieren über cancel(byProducingResumeData:) — nur so lässt sich
  weiterladen statt neu zu beginnen. Bei 40 GB der Unterschied
  zwischen brauchbar und unbrauchbar.
- Zwei Übertragungen gleichzeitig, Folgen in Staffel-/Folgenreihenfolge.
  Alles parallel zu laden hieße: keine Folge wird fertig.
- Speicherlimit und Geräteplatz werden vor dem Annehmen geprüft, ein
  Gigabyte bleibt als Puffer frei.
- Verwaiste Dateien werden beim Start entfernt; sie entstehen, wenn die
  App genau beim Abschluss beendet wird.
- Downloads liegen in Application Support, nicht in Caches (iOS räumt
  den eigenmächtig leer) und sind von der iCloud-Sicherung ausgenommen.

Oberfläche
- Download-Knopf auf Detailseiten mit vier Zuständen in einem Knopf.
- „Alle Folgen laden" für Staffel und Serie; ein abgelehnter Titel
  stoppt den Stapel nicht.
- Downloads-Ansicht mit Belegung, WLAN-Schalter, Pausieren, Fortsetzen,
  Löschen — nach Serie gruppiert.

Offline
- Der Player bevorzugt die lokale Datei, auch ohne erreichbaren Server.
- Beim Herunterladen wird ein Abzug der Serverangaben mitgesichert:
  offline lässt sich nichts nachfragen, und der PlaybackDecider braucht
  Container und Codecs.

Ein Fehler, den die Tests aufgedeckt haben: der Koordinator hielt nur
den ModelContext, nicht den ModelContainer. Ein ModelContext hält seinen
Container nicht am Leben — wird der anderswo freigegeben, stürzt jeder
Zugriff auf einen Eintrag ab, und die Meldung zeigt auf ModelContext.reset
statt auf die Ursache. Jetzt liegt die Zuständigkeit beim Koordinator.

102 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:34:57 +02:00
GuidoandClaude Opus 5 55bcfe2fbf Phase 2: Transcoding, adaptive Bitrate, Spuren, Marker, Sync
Der Decider brauchte keine Änderung — nur eine andere Richtlinie.
Genau dafür war er gebaut.

Transcoding
- TranscodeRequest baut die Parameter für /video/:/transcode/universal.
  directStream=1 lässt Plex nur umpacken, wenn der Bildcodec passt,
  statt pauschal neu zu kodieren.
- Vor dem Start wird der Server gefragt (decision); sagt er nein, steht
  der Grund im Klartext da statt eines schwarzen Bildes.
- Beim Verlassen wird der Transcoder angehalten. Ohne dieses Signal
  rechnet der Server minutenlang für niemanden weiter.
- Die adaptive Bitrate kommt mit HLS: AVPlayer wechselt sie selbst.

Qualitätseinstellungen
- Getrennt für WLAN und Mobilfunk, fünf spürbare Stufen.
  Voreinstellung: zuhause Original, unterwegs 4 Mbit.
- NetworkConditions erkennt Mobilfunk über isExpensive — das deckt auch
  den Hotspot ab, der technisch WLAN ist und trotzdem Volumen kostet.

Spuren und Marken
- Ton- und Untertitelwahl im Player, bevorzugte Sprache in den
  Einstellungen, nachsichtiger Sprachvergleich (de/deu/ger/de-DE).
- Intro- und Abspann-Marken mit Überspringen-Knopf, optional
  automatisch. Bereits übersprungene Marken werden gemerkt, sonst käme
  man nach einem Rücksprung nie ins Intro zurück.
- Nächste Folge inkl. Staffelwechsel; Specials in Staffel 0 werden
  nicht vorgezogen.

Fortschritt
- Alle zehn Sekunden an den Server, plus Schlussmeldung beim Verlassen.
  Erst die nimmt den Titel aus „wird gerade abgespielt" heraus.
- markWatched/markUnwatched/rate/refreshLibrary stehen bereit.

86 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:24:17 +02:00
GuidoandClaude Opus 5 d0a84f35de Drei Fehler aus dem ersten Praxistest
Anmelde-Adresse wurde doppelt kodiert
  URLComponents.fragment kodiert beim Setzen erneut, aus %5B wurde
  %255B. Plex antwortete mit „Diese Anfrage konnte nicht abgeschlossen
  werden". Die Adresse wird jetzt von Hand zusammengesetzt.

Verbindungssuche hing bis zu anderthalb Minuten
  probeTimeout war deklariert, aber nirgends angewendet — es galt der
  URLSession-Standard von 60 Sekunden. Zusammen mit der Prüfung Rang
  für Rang kostete eine schweigende lokale Adresse die volle Zeit,
  bevor der direkte Weg überhaupt begann. Jetzt laufen alle Adressen
  gleichzeitig, mit Zeitgrenze, und die Rangordnung entscheidet unter
  den erreichbaren.

Kein Zurück während des Ladens
  Die Kopfzeile mit dem Schließen-Knopf wurde nur im Zustand .playing
  gezeichnet. Bei langsamer Verbindung saß man ohne Ausweg vor einem
  Ladekreis. Sie liegt jetzt als eigene Ebene darüber und erscheint
  auch beim Vorbereiten — samt Titel, damit erkennbar bleibt, worauf
  gewartet wird.

Sechs neue Tests, darunter drei, die die Zeitgrenze der
Verbindungssuche an einer schweigenden Adresse nachstellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:17:18 +02:00
GuidoandClaude Opus 5 9a69df4635 Phase 1: Anmeldung, Bibliothek, Detailseiten, Hybrid-Player
Die Datenschicht:
- plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund
- Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor
  Vermittlung), mit Abgleich der Server-Kennung gegen veraltete
  plex.direct-Adressen
- PlexServer als einzige Stelle, an der die App Endpunkte kennt:
  Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten,
  Staffeln/Folgen, Suche

Die Oberfläche:
- Startseite mit Aufmacher und den Reihen vom Server
- Mediathek als Raster oder Liste, mit Filter- und Sortierblatt
- Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung,
  Fassungsauswahl und aufklappbaren technischen Angaben
- Suche mit gemerkten Anfragen
- Vollbildplayer mit eigenem Chrome

Der Player:
- AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll
- VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test`
  in FlixyKit nicht das 920-MB-Binärpaket mitzieht

Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf,
dass `strong=true` einen dreißigstelligen Code liefert, der sich bei
plex.tv/link nicht eintippen lässt — jetzt der kurze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:46:28 +02:00