261cdee2f21f11469647a573e0a3ffe0e28c10ed
50
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e86c0a6e95 |
Ein Download riss ab, galt als fertig — und setzte nie fort
Dein Bericht: bei rund 3 GB abgebrochen, beim Fortsetzen wieder bei null,
und später „plötzlich bei 60 Prozent fertig". Beide Hälften standen so im
Code.
**Fertig war sie nicht, sie war halb.**
if expected > 0, Double(written) < Double(expected) * 0.5 { verwerfen }
Alles über der Hälfte galt als vollständig. Diese Regel war nie für diesen
Fall gedacht, sondern für die **hochgerechnete** Größe der Transcoder-Stufen
(Bitrate × Dauer) — dort ist Abweichung normal. Beim Original nennt der
Server die Dateigröße in den Metadaten, und da ist jedes fehlende Byte ein
Abriss.
Das unterscheidet jetzt `expectedIsExact`, gesetzt beim Planen und
mitgeführt am Eintrag. Dazu merkt sich der Koordinator, für welche Titel der
Server eine `Content-Length` geschickt hat — auch das ist eine Zusage.
**„Fortsetzen" war eine Zusage, die der Code nicht halten konnte.**
`NSURLSessionDownloadTaskResumeData` kam im ganzen Projekt nicht vor; den
Wiederaufnahme-Datensatz gab es nur beim Pausieren von Hand. Nach einem
Netzfehler war er weg, und der Knopf daneben hieß trotzdem „Fortsetzen".
Jetzt wird er aus dem Fehler gerettet, und wo es ihn nicht gibt, heißt der
Knopf „Erneut versuchen".
**Und es merkte niemand, wenn das WLAN zurückkam.** `RootView` reicht das
jetzt weiter — nur für Einträge mit Wiederaufnahme-Datensatz, damit ein 404
sich nicht im Kreis versucht.
**Rate und Restzeit** stehen in der Zeile: „12 MB / 20 MB · 3,4 MB/s ·
gleich fertig". `DownloadRate` ist ein reiner Wertetyp mit Uhr von außen —
geglättet, weil rohe Differenzen um ein Vielfaches springen, und ohne Zahl,
solange nichts gemessen ist. Neun Tests.
**Zwei Dinge, die erst die Messung gezeigt hat** — und die beide nicht in
der App liegen:
Erstens: reißt eine Übertragung ab, **die eine `Content-Length` angekündigt
hatte**, wiederholt die Hintergrundsitzung von iOS sie selbst — stumm, ohne
den Delegaten zu fragen, und **jedes Mal ab Byte null**. Gemessen: sieben
Anläufe, jeder die vollen 12 MB. Das ist genau dein „fing wieder bei null
an", und dagegen hilft kein Wiederaufnahme-Datensatz.
Zweitens: ein Fortsetzen braucht eine Prüfmarke vom Server. Ohne `ETag`
oder `Last-Modified` kann iOS sein `If-Range` nicht stellen und lädt alles
noch einmal — gemessen, 20 MB zweimal. Mit Marke kam die zweite Anfrage als
`Range: bytes=5636096-` und holte nur den Rest.
Die Attrappe kann das jetzt alles nachstellen: eine beliebig große Datei,
die auf Wunsch mittendrin abreißt, mit oder ohne angekündigte Länge, und
gedrosselt. Ohne sie ist nichts davon beweisbar — und genau daran ist der
Fehler vorbeigekommen.
Im Simulator gegen die Attrappe: 12 von 20 MB → „Fehlgeschlagen:
Übertragung unvollständig", nicht Haken. Vollständig → Haken. iOS und tvOS
bauen, 489 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a665d79e6d |
Antippen spielt ab — und vier Dinge, die daneben lagen
**Antippen startet, langes Drücken führt auf die Detailseite.** Am Telefon und am iPad; am Mac bleibt der Klick die Seite, denn dort gibt es den Abspielknopf beim Überfahren und langes Drücken ist keine übliche Geste. Die Hürde war nicht die Geste, sondern das Schieben: ein `NavigationLink` schiebt selbst, ein Menüeintrag kann es nicht. Der Stapel ist deshalb ein eigener Typ mit Pfad (`MediaStack`) und reicht eine Schiebefunktion über die Umgebung nach innen — denselben Weg nimmt schon die Meldung, dass sich am Server etwas geändert hat. **Zwei Knöpfe übereinander auf der Detailseite.** Das Monogramm hing als Überzug an `mediaStack` — also über **jedem** Bildschirm dieses Stapels, auch den geschobenen, und dort über deren Drei-Punkte-Menü. Es gilt jetzt für die Startseite und endet mit ihr. **Der Kopf der Detailseite war viel zu flach.** Dort stand kein Seitenverhältnis, also galt die Vorgabe 16∶9 — während `HeroArtwork` auf einem schmalen Gerät ein **Plakat** anfordert, also hochkant. Zu sehen war ein Streifen aus der Mitte. Der Aufmacher auf der Startseite macht es seit Langem richtig; die beiden Detailseiten nicht. **Die Mediathekenleiste stand linksbündig in einer halbleeren Kapsel.** Jetzt ist sie so breit wie ihr Inhalt und mittig. `ViewThatFits` nimmt die ungescrollte Reihe, solange sie passt, sonst die scrollbare — damit steht nirgends eine Zahl, ab wann es kippt. **Und der Grund, warum die Mediathekenauswahl beim Nutzer nie griff:** Plex. Dessen Hubs sagten nicht, aus welcher Mediathek sie stammen, also warf der Filter jede Reihe weg und fiel auf „alles" zurück — ein Schalter, der nichts tut. `PlexHub` liest jetzt `librarySectionID`, als Zahl oder als Zeichenkette, je nachdem was der Server liefert. Vier Tests dazu. Das ist **kein** Wiederaufnehmen der Plex-Arbeit: es repariert eine Funktion, die dort gelogen hat. Neues kommt weiterhin nicht dazu. iOS und tvOS bauen, 476 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7f380e5f4d |
Die Bibliothek beginnt jetzt in einer Mediathek
Vorher stand dort eine Liste aller Mediatheken: auf „Bibliothek" tippen, dann auf „Filme", dann endlich ein Raster. In eine andere wechseln hiess zurück und wieder hinein. Der Zwischenbildschirm hat nie etwas erzählt, was eine Leiste nicht auch zeigen kann — er war ein Platzproblem, kein Entwurf. Jetzt steht man sofort in einer Mediathek, und darüber der unteren Leiste liegt eine zweite mit den Mediatheken und Live-TV. Auf dem iPad und am Mac ändert sich nichts: dort steht jede Mediathek einzeln in der Seitenleiste, auf dem Fernseher nebeneinander in der oberen Leiste — es ist dieselbe Idee in der Form, die ein Telefon hergibt. **Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis, dessen Einträge zwei Navigationsleisten übereinander tragen. Genau daran ist der Zwischenbildschirm seinerzeit entstanden. Eine selbst gezeichnete Leiste kennt diese Grenze nicht — und sie sieht aus wie die Insel auf dem iPad, weil sie dieselbe Fläche benutzt: `FlixySidebar.surface(in:)` ist ein Bauteil, kein nachgebautes Aussehen, und wird jetzt zum dritten Mal gerufen. Als Sicherheitsrand und nicht als Überzug, damit die Liste über der Leiste endet statt dahinter zu verschwinden. Bei mehr als vier Mediatheken wird sie waagerecht scrollbar. **Das Monogramm steht jetzt in der Werkzeugleiste**, am rechten Ende, auf derselben Höhe wie Suche, Filter und Ansicht — die rücken dafür nach links. Als Überzug lag es über dem Inhalt, auf einem Raster also über einem Plakat. Dass es dabei an den Anfang der Reihe rutschte, hat einen Grund, der im Quelltext steht: SwiftUI reiht Werkzeugleisten von aussen nach innen, ein von aussen angehängter Eintrag steht deshalb **vor** den übrigen. Wo in der Reihe etwas steht, kann nur der entscheiden, der die Reihe schreibt. Auf der Startseite bleibt es ein Überzug — dort gibt es keine Navigationsleiste. Dazu: die Zuordnung Mediathekenart → Symbol stand dreimal im Projekt, eine davon falsch (jede Mediathek bekam „film", auch eine mit Serien). Jetzt einmal, neben den übrigen Ableitungen aus den Modellen. Geprüft im Simulator: „Bibliothek" öffnet sofort das Raster, die Leiste wechselt zwischen Filme, Serien, Gemischtes und Live-TV, und das Monogramm steht ganz rechts in der Werkzeugleiste. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0aa11e1c25 |
Die Einstellungen verlassen die Leiste — oben rechts steht jetzt das Konto
Auf dem iPhone belegten die Einstellungen einen von fünf Plätzen in der unteren Leiste, gleichrangig neben Start und Bibliothek. Das unterstellt, dass man zwischen ihnen hin und her wechselt, und das tut niemand. Jetzt stehen dort drei Einträge — Start, Bibliothek, Downloads — und der Weg in die Einstellungen führt über das Monogramm oben rechts. Auf dem iPad und am Mac bleibt alles, wie es war: dort hat die Seitenleiste Platz, und den Weg über das Konto gibt es ohnehin schon. Drei Entscheidungen, die nicht offensichtlich sind: **Ein Überzug, kein Werkzeugleisten-Knopf.** Die Startseite schaltet ihre Navigationsleiste ganz ab — sie war leer und belegte trotzdem Höhe —, ein `ToolbarItem` erschiene dort gar nicht. Der Knopf liegt deshalb einmal über allen Bildschirmen statt dreimal in deren Leisten und sitzt überall an derselben Stelle. Beim Scrollen blendet er sich aus, wie die Insel auf dem iPad: über einem Film, den man gerade sucht, hat ein Kontoknopf nichts verloren. **Ein Blatt, kein Bereich.** `SettingsView` ist ein Formular aus geschobenen Unterbildschirmen und braucht nur einen Navigationsstapel um sich; die Medienziele aus `mediaStack` ruft es nie auf. Beim Schliessen steht man wieder dort, wo man war. **Ein Buchstabe, kein Bild.** Den Avatar gab es schon — er steckte in der Kontozeile der Seitenleiste und wird jetzt von beiden benutzt. Ein Serverbild führt das Konto heute nirgends mit, weder bei Jellyfin noch bei Plex; eine graue Silhouette, die bei jedem gleich aussieht, sagt weniger als der Anfangsbuchstabe. Dazu gehört `.settings` in die Rückfallliste beim Grössenwechsel: wer am iPad in den Einstellungen steht und das Fenster schmal zieht, stünde sonst vor einem Bereich, den die Leiste gar nicht mehr führt. Geprüft im Simulator: drei Einträge unten, Monogramm oben rechts, Tippen öffnet die Einstellungen, Schliessen führt zurück. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51abcabb27 |
Die Rechnung nachgetragen — und eine falsche Erklärung zurückgenommen
Eine unabhängige Gegenprüfung (zweiunddreissig Agenten, jede Behauptung
von Skeptikern angegriffen) bestätigt die Ursache und rechnet alle vier
gemessenen Zahlen nach:
scrollbarer Teil 421,0 (8 Zeilen à 36,33 + Überschrift 22,33
+ 8 Zwischenräume à 12 + 12 unten)
starrer Rest 137,6 (Kopfzeile 48,33 + Trenner 1
+ Kontozeile 64,27 + 2 × 12 aussen)
zusammen 558,6 gemessen 558,67
zentriert (420 − 558,67) / 2 = −69,335 gemessen −69,33
Die Rechnung steht jetzt im Quelltext, damit beim nächsten Eintrag in der
Leiste jemand nachsehen kann, wie viel Luft noch ist: starr bleiben 137,6
von 420.
**Und eine Berichtigung.** Ich hatte den Zwischenwert 620,67 einer
Rückkopplung zugeschrieben — gemessener Rand geht über `topInset` in
einen `safeAreaInset` und verändert den Rand. Das stimmt nicht. Die
Differenz von zweiundsechzig kommt schlicht aus
`.padding(.top, statusBarHeight)` an der Seitenleiste, das während der
Drehung noch den Hochformatwert trug.
Die Umstellung auf den Fensterwert bleibt richtig und nützlich — sie
spart eine Layoutrunde. Sie war nur nicht die Reparatur, und der
Kommentar sagte etwas anderes. Ein Kommentar, der einen falschen
Mechanismus behauptet, ist schlimmer als keiner: er schickt den nächsten
in die falsche Richtung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
a1ff5cb7bb |
Von der Kachel direkt abspielen, auch bei Serien
Wer weiss, was er sehen will, klickte bisher auf die Kachel, wartete auf die Detailseite und drueckte dort „Abspielen\". Zwei Schritte fuer eine Absicht. Jetzt gibt es oben rechts einen Knopf — auf dem Mac beim Darueberfahren, auf iPad und iPhone ueber langes Druecken. Bei einer Serie ist die Absicht nicht „spiel die Serie\", sondern „mach da weiter, wo ich war\". Welche Folge das ist, entscheidet `EpisodeSelection`: die aelteste ungesehene, fortgesetzt falls angefangen, und von vorn, wenn alles gesehen ist. Ein reiner Typ mit achtzehn Tests — im App-Ziel gibt es kein Unit-Test-Ziel, und „er hat die falsche Folge gestartet\" ist sonst ein Befund, den man nicht nachstellen kann. Ausdruecklich die aelteste ungesehene, nicht die am weitesten fortgeschrittene: wer sich eine Folge aufgehoben hat, soll sie nicht dadurch verlieren, dass er weiter vorn hineingeschaut hat. Dazu `episodes(inSeries:)` im Vertrag — bei Jellyfin eine Anfrage statt dreizehn, fuer alle anderen die Vorgabe ueber `children(of:)`. Nicht `/Shows/NextUp`: der braucht eine Seriennummer, die eine Serie selbst nicht hat, versteckt vollstaendig gesehene Serien und bringt eine eigene Zeitgrenze mit. Welche Folge dran ist, soll eine Regel entscheiden, nicht der Server. Nebenbei behoben: der grosse Abspielknopf im Aufmacher scheiterte bisher mit „keine abspielbare Datei\", sobald dort eine Serie stand — und Jellyfins „Zuletzt hinzugefuegt\" liefert neue Folgen als ihre Serie. Der Knopf liegt als Ueberzug auf dem NavigationLink, nicht darin: ein Button im Label eines Links loest auf iOS nie aus, und `.plain` macht das Versagen lautlos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
24d7423f9c |
Bei Jellyfin galt zuhause als unterwegs
Beinahe-Regression, gefunden bevor sie jemanden traf: `isLocalConnection` kam aus `session.server?.connection.local`, und `server` ist ein `PlexServer` — bei Jellyfin immer `nil`. Jede Jellyfin-Wiedergabe galt damit als entfernt, auch zuhause am Kabel. Der neue Deckel hätte dort ein 67-Mbit-Remux ohne Not auf acht Megabit heruntergerechnet. Jetzt wird gefragt statt geraten: `/System/Endpoint` beantwortet es mit **demselben** Aufruf, mit dem Jellyfin auch selbst entscheidet, ob es eine Bitratenleiter ausliefert. Ein Blick auf die Adresse hätte nicht genügt — wer über einen Reverse Proxy geht, erreicht seinen Server auch von der Couch aus unter einem öffentlichen Namen, und über ein VPN kommt eine private Adresse von weit her. Gefragt wird bei beiden Wegen, beim Anmelden und beim Wiederherstellen: dasselbe Konto kann heute zuhause und morgen im Zug aufwachen. Im Zweifel `false` — lieber einmal zu vorsichtig gedeckelt als eine Wiedergabe, die über eine Fernverbindung gar nicht erst anläuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eebe9280cb |
Die Oberfläche weiß nicht mehr, dass es Plex gibt
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> |
||
|
|
81e22c4f97 |
Hinter der Insel läuft jetzt das Wasser durch
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> |
||
|
|
be4b961641 |
Der Serienname ist jetzt der Weg zur Serie
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> |
||
|
|
34593d9973 |
Die Leiste steht jetzt neben dem Inhalt, nicht darüber
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> |
||
|
|
5b2d902cd9 |
@AppStorage lässt sich nicht animieren
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> |
||
|
|
9a5412dc5d |
Die Einstellungen lagen hinter der Leiste
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> |
||
|
|
ab0adef8d4 |
Kein schwarzer Spalt mehr zwischen Bild und Leiste
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> |
||
|
|
e2779f2519 |
Bearbeiten, Suche auf dem Handy, und zwei Knöpfe, die man treffen kann
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> |
||
|
|
dba645d168 |
Der leuchtende Streifen war die Reihenfolge der Überlagerungen
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> |
||
|
|
873b8a4405 |
Volle Höhe, eine Kante statt eines Streifens
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> |
||
|
|
d0f732402c |
Der Aufmacher endet an der Leiste, und durch sie sieht man nur noch gedämpft
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> |
||
|
|
fbed83b10f |
Die System-Leiste kam auf dem Mac zurück
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> |
||
|
|
fccec1217b |
Eigene Seitenleiste — überall dieselbe
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> |
||
|
|
e7de8a9055 |
Bilder ohne Warten — und der Beweis, dass der Mac die Leiste selbst zeichnet
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> |
||
|
|
7faa16be9c |
Die fremde Info.plist war es, nicht der Build-Ordner
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> |
||
|
|
39d9138911 |
Catalyst: ohne Netz, ohne Übersetzung, ohne Fenstergröße
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> |
||
|
|
4150724394 |
Mac: die Seitenleiste bekommt auch eine Obergrenze
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> |
||
|
|
f8ada44a8c |
Mac: näher am iPad — Schrift, Seitenleiste, Formulare
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> |
||
|
|
95723588c2 |
Nachgesehen — und dabei vier Fehler gefunden, zwei davon meine neuen
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> |
||
|
|
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>
|
||
|
|
3f6c6d8816 |
Konto unten in der Seitenleiste — und eine Einstellung, die es nicht gab
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> |
||
|
|
4a8500fc37 |
Navigation: das baut JellyTV nicht selbst — das ist Apples Bauteil
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> |
||
|
|
5cebfed36b |
Navigation: Glas, oben oder seitlich, und mehr Reihen auf der Startseite
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> |
||
|
|
fb418c842d |
Wiedergabe: der Player spricht nicht mehr mit Plex, sondern mit dem Vertrag
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> |
||
|
|
370f7f94f1 |
Startseite: der Aufmacher wechselt, die Neuzugänge stimmen
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>
|
||
|
|
5696254034 |
Jellyfin: der Anmeldebildschirm bietet jetzt beides an
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> |
||
|
|
fe2d730c00 |
Die Ansichten sprechen nicht mehr mit Plex
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> |
||
|
|
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> |
||
|
|
c2ddc10871 |
Wiedergabe: der alte Motor lief weiter, während der neue startete
Der Ton, der nach dem Schließen der App weiterlief, und die neue Wiedergabe, die stumm blieb, sind derselbe Fehler — und er ist meiner. `stop()` machte drei Netzwerkaufrufe, bevor es den Motor abschaltete: Fortschritt melden, Transcoder anhalten, Tuner freigeben. Beim Wechsel habe ich das in den Hintergrund gelegt, damit man nach dem Antippen nicht wartet. Damit lief der alte Motor die ganze Zeit weiter, und wenn er endlich abräumte, nahm er der bereits laufenden neuen Wiedergabe die Audiositzung weg. Bei Live-TV kam dazu, dass der Motor weiterspielte, während die App längst im Hintergrund war — Hintergrundton ist erlaubt, und niemand hatte ihn abgeschaltet. Jetzt ist beides getrennt. `silence()` legt den Motor sofort still, ohne Netzwerk und ohne Warten; `releaseOnServer()` meldet danach in Ruhe, dass Schluss ist. Beim Wechsel wird zuerst stillgelegt, dann gebaut — nie laufen zwei Motoren gleichzeitig. Dazu eine Grenze für ewiges Laden: ein Live-Strom kann sterben, ohne dass der Motor es merkt, und der Kreisel drehte sich dann bis zum nächsten Antippen. Nach vierzig Sekunden ohne Daten wird abgebrochen, der Motor stillgelegt und gesagt, was los ist — sonst bliebe bei Live-TV obendrein der Tuner belegt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
7cfc6e0667 |
Akzentfarbe zieht sich durch
Picker-Werte leuchteten weiter in der Standardfarbe, während Schalter und Symbole daneben schon die gewählte trugen. Zwei Ursachen, beide behoben — welche davon im Einzelfall zog, ließ sich nicht nachstellen, weil der Bildschirm hinter der Anmeldung liegt. Erstens stand `.tint` im Scene-Body. Scene-Bodies werten Änderungen an beobachteten Objekten nicht zuverlässig neu aus; nach einem Farbwechsel blieb ein Teil der Oberfläche auf dem alten Wert stehen. Jetzt setzt RootView Tönung und Akzent — eine View wertet zuverlässig neu aus. Zweitens nahm der Vorgabewert der Umgebung blind die Standardfarbe. Listen und Formulare zeichnen ihre Zeilen teilweise in einem eigenen Darstellungskontext, in dem eigene Umgebungswerte verloren gehen; dort erschien dann Mint statt der gewählten Farbe. Der Vorgabewert liest jetzt die gespeicherte Auswahl. Damit das nicht wieder auseinanderläuft, liegt die Speicherung an einer Stelle: AccentOption.stored/store. Vorher hatte FlixyTheme eigene Schlüssel — zwei Kopien derselben Namen driften auseinander, und zwar unbemerkt. Dazu setzen die Formulare die Tönung ausdrücklich, statt sich auf Vererbung zu verlassen. 154 Tests, fünf davon zur Speicherung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
1756ed11cc |
Anmeldung über die Plex-Seite statt über einen Code
Kein Code mehr zum Abtippen. Die Anmeldemaske kommt von Plex und öffnet sich über ASWebAuthenticationSession in einem Fenster innerhalb der App; nach dem Anmelden schickt Plex über forwardUrl auf ein eigenes Schema zurück, das Fenster schließt sich von selbst. Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem Plex einen Token herausgibt. Sichtbar ist er nicht mehr. Was das bringt: - Kein Fernseher-Gefühl mehr. - Zwei-Faktor läuft mit, weil Plex die Seite selbst ausliefert. - Das Passwort erreicht Flixy nie. Steht auch so unter dem Knopf. - prefersEphemeralWebBrowserSession = false: wer in Safari schon bei Plex angemeldet ist, braucht nur zu bestätigen. Zwei Details, die sonst still scheitern: - Die Rückadresse wird strenger kodiert als die übrigen Werte. `:` und `/` lässt urlQueryAllowed durch, und für Plex endete der Wert dann beim ersten Schrägstrich — der Rücksprung käme nie. - Das Schema muss in CFBundleURLTypes stehen, sonst kennt iOS es nicht und das Fenster bliebe offen stehen. Als Schema die Bundle-Kennung statt eines kurzen Worts wie „flixy": ein kurzes könnte eine andere App beanspruchen und die Anmeldung abfangen. 132 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c0c1d651d |
Phase 6: Live-TV, plus Live Activity für Downloads
Live-TV - An keinem echten Server geprüft: die DVR- und EPG-Endpunkte sind der am schlechtesten dokumentierte Teil der Plex-API. Die Modelle sind deshalb durchgehend nachsichtig — fehlende Felder ergeben leere Listen, nicht Fehler, und die Senderkennung wird als Zahl, Zeichenkette oder uuid akzeptiert. - Ohne eingerichteten Empfänger erklärt die App das, statt einen Fehler zu zeigen. Das ist der häufigste Fall. - Senderliste mit laufender Sendung, Fortschrittsbalken und „danach". Der Programmführer wird getrennt geholt: fällt er aus, bleibt das Umschalten möglich. - Die Anbieterkennung wird aus /media/providers gelesen statt fest verdrahtet — sie wechselt je nach Server. Live Activity - Eigene Widget-Erweiterung; iOS zeichnet den Sperrbildschirm in einem getrennten Prozess, während die App meist gar nicht läuft. - Der Presenter drosselt auf 1,5 Sekunden. ActivityKit verwirft zu häufige Aktualisierungen kommentarlos, und der Fortschritt kommt mehrmals je Sekunde — ungebremst bliebe die Anzeige stehen. - @preconcurrency import: Activity und ActivityContent tragen keine Sendable-Zusage. Zwei Stolpersteine unterwegs: canImport(ActivityKit) ist auf macOS wahr, die Typen darin aber nicht verfügbar — es muss os(iOS) sein. Und eine von Hand geschriebene Info.plist ohne CFBundleIdentifier führt zu einer Erweiterung ganz ohne Kennung; der Fehler lautet dann „not prefixed with the parent app's bundle identifier" und zeigt auf die falsche Stelle. 126 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
1ed64bf166 |
Phase 0: Projektgerüst, Design-Tokens und Navigation
Legt das Fundament für den Plex-Client an: - XcodeGen-Projekt nach dem Muster von Onyx (com.scarriffleservices, strict concurrency, Projektdatei nicht eingecheckt) - FlixyKit mit vier Modulen: PlexKit, FlixyDesign, FlixyPlayback, FlixyDownloads. macOS steht als Plattform mit drin, damit die Logik-Tests ohne Simulator laufen. - FlixyDesign: dunkle Farb-Tokens, Schriftskala, Poster- und Querformat-Kacheln, Aufmacher-Bild. Die Akzentfarbe fließt über die Umgebung und wird per WCAG-Rechnung lesbar gehalten. - PlexKit: Kopfzeilen-Identität, Transport-Protokoll, Fehlerarten und die Medienmodelle samt nachsichtigem Decoder. - FlixyPlayback: VideoPlayerEngine-Protokoll und PlaybackDecider — die eine Funktion, an der Phase 2 das Transcoding andockt. - Ausfahrbare Seitenleiste auf dem iPhone, Split-View auf dem iPad. - 45 Tests, alle grün. VLCKit 4.0.0-a24 ist vorab geprüft: baut über SPM unter Swift 6 mit strict concurrency. Die iOS-Slice ist mit 239 MB allerdings gewichtig, deshalb bekommt sie in Phase 1 ein eigenes Target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |