- Tabs: Meine Musik, Suchen, Download, Favoriten (vorher Download vor Suchen)
- "Meine Musik": Schnellzugriffe (Favoriten/Wiedergabelisten/Zuletzt) einzeln
über die Einstellungen abschaltbar
- Audio- und Benachrichtigungs-Berechtigung werden jetzt beim ersten Start
gemeinsam angefragt statt erst bei Bedarf; Hinweisbanner + Einstellungen-
Button bei dauerhafter Verweigerung (auch für Benachrichtigungen neu)
- Cover-Extraktion beim Android-Scan von 512px auf 1024px erhöht, für ein
schärferes Cover im Sperrbildschirm
- Fix: notifyListeners() in LibraryService nur noch bei echter Wertänderung
(verhinderte einen Test-Hang durch den neuen Start-Berechtigungs-Aufruf)
557 Tests grün, flutter analyze ohne Befund.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM2JK5mV7AL1g2Rt6H6h9w
Die vier zuletzt zurückgestellten Punkte aus der UX-Simulation:
- Abbrechen-Knopf für hängende YouTube-Downloads (Future.any gegen ein
Abbruch-Signal, Anfrage läuft im Hintergrund aus statt die App zu
blockieren)
- Punkt am inaktiven Download-Unterreiter, wenn dort etwas im Hintergrund
läuft (SubTabs bekommt einen optionalen badges-Parameter)
- Versionsnummer unter "Über Melo" (neue Abhängigkeit: package_info_plus)
- Mini-Player führt beim Wischen jetzt sichtbar mit und wertet zusätzlich
zum Schwung auch die zurückgelegte Strecke aus — ein langsames, aber
weites Wischen wurde vorher komplett ignoriert
Dabei drei bestehende Widget-Tests korrigiert, die den Download-Tab ohne
Provider<DownloadService> aufbauten (hauptmenue_test.dart,
home_shell_test.dart, online_screen_test.dart) — lief bisher nur, weil
niemand danach fragte; das neue Badge tut das jetzt app-weit.
550 Tests grün (vorher 539), flutter analyze ohne Befund.
Zweiter Block aus der UX-Simulation (39 Funde insgesamt):
- Songtext-Icon deaktiviert statt totem Knopf, wenn kein Ziel bekannt ist
- Sleep-Timer-Restzeit jetzt auch im Sheet sichtbar, nicht nur im Tooltip
- Drag-Handle in der Warteschlange, Wisch-nach-unten schließt den
Vollbild-Player
- Fehlender Tooltip am Songtext-Sheet-Schließen-Knopf ergänzt
- YouTube-Download: Zeitüberschreitung meldet sich anders als toter Server
- Server-Tab zeigte beim Öffnen kurz fälschlich "nicht verbunden"
- Einstellungen: Favoriten-/Playlisten-Sync von "Speicher" zu
"Musikserver" verschoben, Navidrome-Eintrag erklärt jetzt seinen Zweck
- Kommentar in geteilte_adresse.dart korrigiert (Verhalten unverändert,
dafür gibt es einen Test)
Bewusst zurückgestellt: ServerFavoriteButton bleibt lokal-only (dokumentierte
Architektur-Entscheidung, siehe G3 im UX-Review), Live-Wischgeste am
Mini-Player, YouTube-Download-Abbrechen, Download-Badge im inaktiven
Unterreiter, Versionsnummer (neue Abhängigkeit nötig).
539 Tests grün (vorher 537), flutter analyze ohne Befund.
7 Agenten sind die App als normaler Nutzer durchgegangen (Erststart,
Bibliothek, Download, Suche, Favoriten, Player, Einstellungen) und haben
39 Reibungspunkte gefunden. Erster Block:
- Mehrfachauswahl lief nur im Lieder-/Favoriten-Reiter, nicht in
Kategorie-/Künstler-/Album-Ansichten — jetzt überall, per geteilter
auswahl_leiste.dart
- Zwei unterschiedlich ausgestattete Favoriten-Listen (Tab vs. Playlisten)
vereinheitlicht
- Baka-Login zeigt Ladezustand + Fehler im Dialog statt bis zu 15s stumm
zu bleiben (wie der Navidrome-Login)
- Cache leeren, Navidrome-Abmelden, Download einzeln entfernen fragen jetzt
nach
- Download-Fehlermeldung bei toter Serververbindung nennt den echten Grund
statt "War schon heruntergeladen"
- YouTube-Fehlermeldungen sind jetzt rot statt neutralfarben
- Suche schließt die Tastatur nach Auswahl, nennt den Suchbegriff bei
"Nichts gefunden", einzelne Verlaufseinträge sind entfernbar
- Icon-/Text-Korrekturen (Mikrofon-Icon fälschlich bei Speicherzugriff,
"Online"-Tab existiert nicht, ReplayGain-Jargon)
537 Tests grün (vorher 533), flutter analyze ohne Befund.
P3-Liste aus dem UX-Review: Player nimmt Cover-Farbe auf, "Teilen" aus
YouTube trägt die Adresse in den Downloader ein, Android Auto blättert
durch die Bibliothek, Laufweite der Typografie geschärft, Überschriften
vereinheitlicht. 533 Tests grün, flutter analyze ohne Befund.
Die P2-Liste aus dem Review — Kleinigkeiten, die man einzeln nicht
vermisst und zusammen sofort merkt.
- Das Coverbild wandert beim Oeffnen der Vollbild-Wiedergabe an seinen
Platz, statt dass ein neuer Bildschirm aufspringt.
- Wischen am Mini-Player: nach oben oeffnen, seitwaerts Titelwechsel. Mit
Schwelle, weil der Mini-Player zugleich ein Knopf ist und der Finger
beim Tippen fast immer ein paar Pixel wandert. Nach unten bewusst
unbelegt, damit niemand die Wiedergabe versehentlich wegschiebt.
- Kuenstlerliste mit Bildern und Albenzahl; dahinter eine Kuenstlerseite
mit seinen Alben und darunter allen Titeln (dafuer neu: groupByAlbum).
Bei nur einem Album entfaellt die Zwischenstufe.
- Alphabet-Sprung am rechten Rand, nur bei Namenssortierung und ab 30
Titeln. Umlaute zaehlen zu ihrem Grundbuchstaben, Ziffern und Zeichen
unter "#" — dieselbe Regel wie die Sortierung.
- Mehrfachauswahl per langem Druck, mit "Zur Warteschlange" und "Zu
Wiedergabeliste".
- Abgeschaltete Systemanimationen gelten jetzt ueberall, nicht nur bei den
Lauf-Balken.
- Querformat und Tablet: der Player legt Cover und Bedienung nebeneinander
und begrenzt die Textbreite.
Beim Bauen gefunden: der Alphabet-Sprung landete bei absteigender
Sortierung am Ende eines Buchstaben-Blocks statt an seinem Anfang.
Aus dem Code-Review nachgebessert, sieben Punkte: die Alphabet-Leiste
wurde breiter, je weniger Buchstaben sie hatte (bei einem einzigen 528 dp
— die Songliste waere verschwunden); die neue Kuenstlerseite baute alle
Titel sofort und oeffnete damit bei 300 Titeln ebenso viele Abos; die
Bildlauf-Animation im Songtext stuerzte bei abgeschalteten
Systemanimationen ab; die feste Zeilenhoehe war 64 statt 72 und die
Schriftskalierung an 1.0 statt an einer echten Groesse gemessen; bereits
enthaltene Titel wurden in Wiedergabelisten verschoben statt uebersprungen
und trotzdem mitgezaehlt; ein Fehler im Stapel brach still ab; und die
Zurueck-Geste verliess den Bildschirm, statt die Auswahl zu beenden.
Selbst gefunden: in der Kuenstlerliste hing der Datenbank-Strom an
CategoryService und wurde bei jeder Kategorie-Meldung neu aufgesetzt.
Nebenbei aufgeklaert, warum ein neuer Widget-Test endlos hing: Aufbau in
setUp laeuft ausserhalb der Testzone, die drift-Stroeme liefen dort in
echter Zeit, waehrend pumpAndSettle in Testzeit auf sie wartete. Aufbau in
den Testkoerper verschoben. Der aeltere Haenger im BACKLOG hat eine andere
Ursache (db.close() dort nachweislich ausgeschlossen); die Spur ist
notiert.
500 Tests gruen (vorher 457), flutter analyze ohne Befund, Release-APK
gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Die letzten vier Punkte aus dem Review-Bericht.
G2 — Server-Bereich: Ein Tipp auf ein Album spielte sofort alles ab;
hineinsehen ging nicht. Jetzt Album- und Kuenstler-Ansicht mit Cover,
Kuenstler, Titelzahl, Gesamtdauer, "Abspielen"/"Zufaellig" und der
Titelliste; Antippen startet an dieser Stelle. Dazu "Mehr Alben laden" —
die Liste hoerte bei 50 auf, ohne dass etwas darauf hinwies.
NICHT umgesetzt: den Server-Bereich aus dem Download-Tab loesen (das
widerspricht Commit 34d6d8e, "Auf Wunsch von Dustin") und eine
Server-Suche (die Suche bleibt ausdruecklich rein lokal).
G8 — Songtext: Traegt der Text Zeitmarken, laeuft er mit; die aktive Zeile
ist hervorgehoben, Antippen springt an die Stelle. Ohne Zeitmarken bleibt
es eine Textwand — die App schaut nach, statt zu raten.
G10 — Cover bleiben liegen. Eigener Ordner, einmal geholt. Der Schluessel
haengt an der Album-Kennung, nicht an der Adresse: die traegt bei jedem
App-Start ein neues Token, und der Ordner waere sonst gewachsen, ohne je
einen Treffer zu liefern. Keine neue Abhaengigkeit.
G3 — Das Herz ist bei Server-Titeln nicht mehr ausgegraut, sondern
favorisiert am Server (star/unstar) — dort, wo der Favoriten-Abgleich sie
auch wieder abholt. Offen bleibt der schwere Teil: Wiedergabezaehler,
Historie und Kategorien braeuchten fuer Server-Titel eine Zeile in der
Bibliothek.
Aus dem Code-Review nachgebessert (sieben Punkte): das Herz behielt beim
Titelwechsel seinen Zustand; recycelte Listenzeilen zeigten kurz das Cover
der vorherigen; der neue Songtext bekam bei Server-Titeln die
Abspiel-Adresse statt der Server-Kennung und fand deshalb nie einen Text;
ein einmal fehlgeschlagenes Cover blieb die ganze Sitzung grau; "Server
nicht erreichbar" sah aus wie "Album hat keine Titel", womit der
Wiederholen-Knopf unerreichbar war; die Kuenstler-Ansicht waere immer leer
geblieben, weil getArtist die Titel gar nicht mitliefert; und zwei
gleichzeitige Cover-Abrufe konnten ein zerschnittenes Bild hinterlassen.
Selbst gefunden: das Haekchen "schon geladen" fragte pro Zeile die Platte
ab, bei jedem Positionswechsel neu. Jetzt einmal je Bildschirm.
457 Tests gruen (vorher 433), flutter analyze ohne Befund, Release-APK
gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Bisher landete Musik nur zufaellig auf dem Geraet: was man abspielte, blieb
nebenbei liegen. Gezielt "dieses Album fuer die Zugfahrt" gab es nicht. Und
dieser Zwischenspeicher wuchs unbegrenzt — es gab nur "alles loeschen".
Zwei getrennte Ablagen. Bewusste Downloads liegen NICHT im Cache-Ordner:
den darf Android bei Speichernot jederzeit selbst leeren, und genau das
darf einem mitgenommenen Album nicht passieren. Sie liegen deshalb neben
der Datenbank im Support-Ordner. Die Grenze gilt nur fuer den
Zufalls-Cache; Downloads sind unbegrenzt und werden nie verdraengt.
- Download-Pfeil je Album und Kuenstler, mit Fortschritt und Abbruch
- Rueckfrage ab 30 Titeln samt Groessenschaetzung
- Obergrenze wahlbar (aus / 512 MB / 1 / 2 / 4 / 8 GB), Standard 2 GB;
am laengsten nicht Gehoertes weicht zuerst
- Liste "Heruntergeladen" mit Titel, Kuenstler, Groesse, einzeln oder alle
entfernbar (Schema 10)
- Wiedergabe nimmt erst den Download, dann den Zwischenspeicher
Aus dem Code-Review nachgebessert, vier ernste Punkte: "aus" (0 MB)
schaltete das Aufraeumen ab statt das Zwischenspeichern und liess den
Cache unbegrenzt wachsen; wer sich erst waehrend der Sitzung am Server
anmeldete, konnte gar nichts laden; die Verdraengung konnte Dateien
loeschen, die die laufende Warteschlange schon als Quelle eingetragen
hatte (Abbruch mitten im Album, ohne Rueckfall aufs Streamen); und ein
heruntergeladener Titel wurde beim Abspielen ein zweites Mal in den Cache
geholt. Dazu sechs kleinere und der "zuletzt gehoert"-Stempel, der beim
Aufbau der Warteschlange statt beim Abspielen gesetzt wurde.
Nebenbei aufgeraeumt, weil die Dateien ohnehin offen waren: keine Emoji
als Knoepfe mehr, kein Colors.redAccent und kein grey.shade mehr im
ganzen Projekt, und die Einstellungen heissen nicht mehr "Settings".
433 Tests gruen (vorher 411), flutter analyze ohne Befund, Release-APK
gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Bisher scrollte man durch die Bibliothek und musste raten, wo man ist —
die einzige Rueckmeldung war die Leiste ganz unten.
Drei tanzende Balken ueber dem Coverbild des laufenden Titels, sein Name
in Rot. Die Balken liegen auf dem Cover statt daneben: eine vierte Spalte
haette dem Titel Platz genommen und die Zeile bei jedem Titelwechsel in
der Breite springen lassen. Pausiert bleibt die Markierung stehen, die
Balken stehen still. Bei abgeschalteten Systemanimationen ebenfalls — eine
Endlosschleife ist genau das, was diese Einstellung vermeiden soll.
Die Markierung sitzt in SongZeile und gilt damit in einem Rutsch fuer
Meine Musik, Favoriten, Suche, Kuenstler, Kategorie und "Zuletzt
hinzugefuegt". Warteschlange und Wiedergabeliste bauen ihre Zeilen selbst
(dort steht rechts Entfernen statt Herz und Menue) und haben dasselbe
Zeichen einzeln bekommen.
Nur die laufende Zeile hoert auf den Wiedergabe-Zustand: der meldet auch
den Pufferstand und feuert mehrmals pro Sekunde. Haenge jede sichtbare
Zeile daran, baute sich die halbe Liste staendig neu auf.
Aus dem Code-Review nachgebessert: Startwert fuer den Strom, sonst
erschien die Markierung beim Zurueckscrollen einen Frame zu spaet; die
Zeile aendert beim Wechsel nicht mehr die Form ihres Aufbaus, sonst verlor
das Herz daneben sein Abo und sah kurz leer aus; die Wiedergabeliste
bekam die Markierung ueberhaupt erst (der Changelog hatte sie faelschlich
schon behauptet); der Abdunkel-Schleier nimmt den Eckenradius jetzt
entgegen statt ihn fest zu verdrahten.
Testluecke geschlossen, die der Review benannt hat: geprueft waren nur die
Regel und die Balken je fuer sich, die Verdrahtung dazwischen nicht —
genau dort sassen alle drei Fehler.
411 Tests gruen (vorher 396), flutter analyze ohne Befund, Release-APK
gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Der Suchen-Tab war 59 Zeilen: ein Textfeld ueber einer flachen
Liederliste, nur ueber Titel/Kuenstler/Album. Wer "Nightcore" eingab,
fand alle Titel dieses Kuenstlers untereinander, aber weder den
Kuenstler selbst noch eine gleichnamige Kategorie oder Liste.
- Vier Abschnitte mit Ueberschrift und Trefferzahl. Alben stecken in
"Kategorien" — der Album-Name ist in Melo die erste Kategorie eines
Titels, ein eigener Abschnitt waere dieselbe Liste zweimal.
- Treffer am Wortanfang zuerst, dann alphabetisch.
- Entprellt (250 ms); vorher baute jeder Tastendruck alles neu auf.
- Letzte acht Suchen, gemerkt beim Abschicken, mit Loeschen-Knopf.
- Der Tab folgt jetzt dem Aufbau der uebrigen Tabs (war ein Scaffold
mit eigener AppBar innerhalb eines Tabs).
- Bewusst rein lokal, ohne Server-Abfrage.
Herausgeloest, weil es sonst eine dritte Kopie gegeben haette:
TitelListenScreen ("Ueberschrift + Liederliste", stand zweimal wortgleich
im Baum) und SongZeile (die Suche setzt einzelne Zeilen in ihre
Abschnitte; eine ganze SongList waere dort verschachteltes Scrollen).
Aus dem Code-Review nachgebessert: Suche und Zuordnung benutzen dieselbe
Normalform, sonst zeigte ein Treffer "0 Titel" und oeffnete eine leere
Liste; Kategorien ohne lebende Titel fallen weg; alle Abschnitte sind
gedeckelt; die Drift-Stroeme werden nicht mehr je Rebuild neu abonniert;
das Loeschkreuz erscheint sofort statt nach 250 ms.
Tot geworden und entfernt: MeloDb.searchSongs samt Test.
396 Tests gruen (vorher 360), flutter analyze ohne Befund, Release-APK
gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Ergebnis einer Durchsicht der App gegen den Massstab "Apple Music".
Alle fuenf Punkte waren keine fehlenden Funktionen, sondern Versprechen,
die die Oberflaeche gab und der Code nicht hielt.
- POST_NOTIFICATIONS fehlte im Manifest. Auf Android 13+ blieb die
Wiedergabe-Benachrichtigung dadurch unsichtbar. Das Recht wird erst
erfragt, wenn wirklich ein Titel laeuft.
- Der Offline-Schalter wurde nirgends gelesen. Er greift jetzt an allen
drei Wegen in die Warteschlange; uebersprungene Titel werden benannt.
- CoverImage kannte nur file://, und der Server-Browser gab die
Cover-Adresse gar nicht erst mit. Server-Titel waren deshalb immer ein
graues Notensymbol.
- Warteschlange entfernen/umsortieren baute die ganze Queue neu auf:
hoerbarer Aussetzer und doppelte Wiedergabe-Zaehlung. Jetzt mutiert nur
der betroffene Eintrag, die Buchfuehrung steht vor dem Player.
- Das Herz bei Server-Titeln legte einen Favoriten mit leerer Kennung an,
der nie in der Liste auftauchte. Es ist jetzt ausgegraut.
Nachgebessert aus dem Code-Review: Offline-Meldung an die Stelle geholt,
wo sie greifen kann; Queue-Buchfuehrung vor den Player gezogen;
Fehlerpfad fuer die Berechtigungs-Abfrage ergaenzt.
360 Tests gruen (vorher 328), flutter analyze ohne Befund, Release-APK
gebaut und POST_NOTIFICATIONS darin nachgeprueft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
Auf Wunsch von Dustin. Der Inhalt bleibt unveraendert (YouTube-Downloader
und Server-Bibliothek als Unterreiter), nur die Beschriftung aendert sich.
Die vier Bereiche: Meine Musik, Download, Suchen, Favoriten.
- Symbol ebenfalls zurueck auf Icons.download_outlined/download. Das ist
kein neuer Entwurf: genau dieses Symbol trug der Tab, bevor er in
Commit a625d70 in "Online" umbenannt wurde.
- Die Ueberschrift im Bereich selbst stand noch auf "Online" — ein Tab
"Download", der auf einen Bildschirm "Online" fuehrt, waere verwirrend.
Zieht jetzt nach.
- Tests und Testbeschreibungen entsprechend angepasst.
328 Tests gruen (1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
Entwarnung vorweg: es fehlt nichts. Alle vier Bereiche (Meine Musik,
Online, Suchen, Favoriten) standen durchgehend in der Bottom-Nav;
lib/main.dart wurde von der UI-Politur gar nicht angefasst.
Aber die Politur hatte das Menue abgeschwaecht — zwei eigene Fehler:
- selectedLabelStyle/unselectedLabelStyle bekamen beide fontSize 12. Damit
verlor der aktive Eintrag den Groessenunterschied (Flutter-Vorgabe 14 zu
12), an dem man erkennt, wo man ist. Jetzt keine fontSize mehr.
- unselectedItemColor stand auf text3 (Beiwerk-Stufe). Ein Hauptmenue ist
kein Beiwerk -> text2.
Zusaetzlich deutlicher: Haarlinie ueber der Leiste (auf Schwarz ging sie
sonst im Inhalt auf), gefuellte Symbole fuer den aktiven Bereich (zweites
Merkmal neben der Farbe) und fetterer aktiver Eintrag.
Neu: test/hauptmenue_test.dart — prueft Vorhandensein, Sichtbarkeit auf dem
Bildschirm, Position am unteren Rand, Wechsel per Tipp und die Abhebung des
aktiven Bereichs. Damit kann keine weitere Politur das Menue unbemerkt
abschwaechen.
Nebenbei eine Test-Falle geloest: await db.close() nach einem tester.tap
laesst den Lauf unbegrenzt haengen (drift plant eine Aufraeum-Aufgabe ein,
die der Test-Rahmen nicht mehr abarbeitet). Rezept im Test dokumentiert.
Beim bekannten Haenger in song_detail_sheet_test.dart hilft es NICHT —
gegengeprueft und als eigene Ursache im BACKLOG vermerkt.
328 Tests gruen (10 neue, 1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
Kein Redesign — Schwarz/Rot, 4 Tabs und Lieder/Kategorie bleiben. Grundlage
sind die Design-Skills: ui-ux-pro-max (Barrierefreiheit, Touch, Typografie,
Bewegung), ui-design/mobile-android (Material 3) und die Token-Disziplin
aus Hue.
Behobene Maengel (messbar, nicht Geschmack):
- Text in Colors.white38 an 17 Stellen = 3,4:1 Kontrast, WCAG verlangt
4,5:1. Ersetzt durch drei benannte Stufen (text1 15,9:1 / text2 8,8:1 /
text3 4,9:1). theme_kontrast_test.dart rechnet die Verhaeltnisse bei
jedem Lauf nach, statt sie zu behaupten.
- SubTabs waren ~38 dp hoch (Material 3 verlangt 48), ohne Ripple und ohne
Uebergang. Jetzt 48 dp, InkWell, AnimatedContainer, und die Auswahl wird
Vorlesehilfen als Zustand gemeldet (Semantics.selected).
- mini_player.dart: Expanded UM eine feste Hoehe herum — zwei
widerspruechliche Angaben. Entschaerft.
- Emoji in Bedienelementen der Einstellungen (jeweils neben einem echten
Icon) entfernt.
Neu in shared/theme.dart: MeloSpace (8er-Raster), MeloRadius, MeloMotion,
Farbrollen text1/2/3 + border + hairline, minTouchTarget, vollstaendiges
textTheme (7 Stufen) und Themes fuer Listen, Sheets, Snackbars,
Fortschritt, Trenner.
318 Tests gruen (23 neue, 1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
Neu: BACKLOG.md. Haelt fest, was bewusst zurueckgestellt wurde, damit es
nicht stillschweigend liegen bleibt.
- song_detail_sheet_test.dart haengt und ist uebersprungen: inklusive der
vier bereits widerlegten Verdachtsmomente (runWithClient erreicht das
Sheet, drift unter testWidgets, einzelne Provider) und dem naechsten
Schritt. Fachlich deckt metadaten_uebernahme_test.dart den Zweck ab.
- Kein Wiederherstellen einzeln geloeschter Titel (Papierkorb am Server
waere denkbar, bewusst nicht gebaut).
Das TODO im Test verweist jetzt auf die Datei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
Auf Wunsch von Dustin: seine beste Freundin sortiert ihre Sammlung ueber
das Album-Feld. In Melo ist der Album-Titel deshalb ab jetzt die Kategorie
— sie muss nichts neu machen.
1) Album = Kategorie
- kategorienAusTags(): Album-Titel steht VORN in der Kategorienliste (die
erste Kategorie bestimmt das Coverbild), Genres dahinter.
- Beide Scans (android_scan + scan_service) tragen ihn ein; von Hand
gepflegte Kategorien (categoriesEdited) bleiben unberuehrt.
- DB-Schema 9: einmalige Nachruestung bestehender Bibliotheken, damit das
nicht erst beim naechsten vollstaendigen Scan sichtbar wird (der auf
Android nur laeuft, wenn sich die Dateianzahl aendert).
2) Reiter: 'Songs/Kuenstler/Alben' -> 'Lieder/Kategorie/Kuenstler'
- Neu: library/category_list.dart mit groupByCategory(); Lieder ohne
Kategorie sammeln sich am Ende unter "Ohne Kategorie".
- Entfernt: library/album_list.dart, groupByAlbum(), albumArtistLabel() —
mit dem Alben-Reiter tot geworden. Das Album-FELD bleibt erhalten.
3) YouTube-Downloads ohne Original-Album
- Feld "Kategorie (optional)" im YouTube-Bereich, mit Vorschlaegen aus der
Bibliothek und freier Eingabe.
- ordneDownloadEin() verwirft nach dem Scan das Album-Tag (yt-dlp leitet es
aus Kanal/Playlist ab — als Kategorie waere das Unsinn) und setzt
stattdessen die gewaehlte Kategorie. Beides als "von Hand gesetzt"
markiert, damit der naechste Scan es nicht zurueckholt.
- MeloDb.songByPath() und MeloDb.verwirfAlbum() neu.
- CategoryService.alleNamen: Kategorienamen ohne zusaetzliche Abfrage —
ohne das flackerte die Vorschlagsliste und im Widget-Test blieb ein
Aufraeum-Timer von drift haengen.
295 Tests gruen (19 neue, 1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
Auf Wunsch: was vom Handy hochgeladen wird, soll auch in Navidrome
auftauchen. Die Subsonic-API kennt keinen Upload — also erledigt es die
Melo-Cloud selbst.
Server (/home/dustin/scripts/melo_cloud.py, nicht versioniert — Backup als
melo_cloud.py.bak-20260821-084506):
- verknuepfe_navidrome(): Hardlink der Registry-Datei nach
/home/dustin/navidrome/music (Bind-Mount des Containers). Gleiche
Partition -> kein zusaetzlicher Speicher. Fallback: Kopie.
- rescan_navidrome(): Subsonic startScan.view ueber Navidromes
AuthProxyHeader (TrustedSources 127.0.0.1) — kein Passwort im Code.
Nur zur Beschleunigung, der Watcher findet neue Dateien ohnehin.
- upload() verknuepft neue UND bereits bekannte Titel (heilt Bestand).
- handle_delete() entfernt die Datei aus Registry + Navidrome, sobald kein
Konto sie mehr aktiv hat. Der Registry-EINTRAG bleibt stehen: handle_list
haengt die Grabsteine daran (JOIN registry) — ohne ihn erfuehren die
anderen Geraete nie von der Loeschung (Zombie-Song).
- registry_pfad(): gemeinsame Dateisuche ueber alle Audio-Endungen, mit
Rueckfall auf den Navidrome-Ordner.
Nebenbei repariert:
- Alle 325 Cloud-Dateien lagen nur noch im Navidrome-Ordner, REG war leer:
jeder Download antwortete "File missing". Per Hardlink zurueckverknuepft
(scripts/melo_cloud_migriere_registry.py).
- upload() legte jede Datei als ".mp3" ab, auch m4a/flac. Jetzt echte
Endung; _download liefert passenden Content-Type und einen Dateinamen
ohne doppelte Endung.
App:
- MeloCloudService.herunterladen() gibt die geschriebene Datei zurueck und
leitet die Endung aus dem Content-Type ab (endungFuer) — sonst landet
eine M4A als .mp3 auf dem Handy und Android ordnet sie falsch ein.
- loeschBremseGreift(): der Abgleich reicht keine Loeschwelle mehr zum
Server durch (>10 Titel UND >1/3 des Serverbestands). Ohne die Bremse
haette eine nicht eingehaengte Speicherkarte die Sammlung auf allen
Geraeten geloescht.
276 Tests gruen (1 uebersprungen), flutter analyze ohne Befund, plus ein
Durchlauf gegen den echten Server (scripts/test_melo_cloud_navidrome.py):
Upload -> Registry + Navidrome (ein Hardlink) -> Navidrome liest ein ->
Loeschen entfernt beides, Grabstein bleibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
BUG 1 — Lieder vom Handy waren nicht abspielbar
("Wiedergabe fehlgeschlagen: (0) Source error" / "Loading interrupted")
Wurzel-Ursache: MeloAudioHandler.loadPlaylist hat jeden Warteschlangen-
Eintrag durch NavidromeService.streamAndCacheToLocal(item.id) geschickt,
sobald Zugangsdaten existierten. item.id ist aber NIE eine Navidrome-Song-ID
— lokal ist es file:///storage/..., beim Server die fertige Stream-Adresse.
Der Server bekam also 'file:///...' als Song-ID, antwortete mit einem Fehler,
und diese Fehlerantwort wurde an just_audio weitergereicht (Source error) —
bzw. als .mp3 in den Cache geschrieben, wodurch der Titel dauerhaft kaputt
blieb.
Zweite Ursache: die Schleife lud die GANZE Warteschlange seriell vorab
herunter (30s Timeout je Titel), bevor setAudioSources lief. Bei hunderten
Titeln startete die Wiedergabe deshalb nie; ein zweiter Tipp brach den
laufenden Ladevorgang ab ("Loading interrupted").
Fix:
- Server-Titel tragen ihre ID in MediaItem.extras['navidromeId'] statt sie
aus der Abspiel-Adresse zu raten. Neue reine Funktionen navidromeIdOf,
songIdOf, nutztServerCache, quelleFuer.
- loadPlaylist baut die Quellen ohne Netzzugriff; Caching des laufenden
Titels im Hintergrund (unawaited).
- Resume/Scrobble nur noch mit der jeweils passenden ID (Server bzw. lokal).
- ladeInCache() ersetzt streamAndCacheToLocal(): .part-Datei, Pruefung des
Inhaltstyps (istAudioAntwort), stabiler Cache-Schluessel ueber die
Song-ID statt der Stream-Adresse (die trug Token+Salt und war je Sitzung
anders — der Cache war nie wiederauffindbar), Client wird geschlossen.
BUG 2 — kein Abgleich zwischen Handy und Server
Neu: services/melo_cloud_service.dart + services/sync_service.dart gegen
cloud.baka-net.de (Bearer-JWT ueber BakaAuth). Server-Titel herunterladen
(offline verfuegbar), eigene Dateien hochladen, Loeschungen in beide
Richtungen (Tombstones), Favoriten und Wiedergabe-Verlauf. Automatisch beim
App-Start und bei Rueckkehr in die App (max. alle 15 Min), plus Knopf unter
Einstellungen -> Geraete-Abgleich. Reine Planungsfunktion planeSync().
Bewusst NICHT ueber Navidrome: die Subsonic-API kennt keinen Upload-
Endpunkt. Navidrome bleibt die Streaming-Bibliothek, die Melo-Cloud ist der
gemeinsame Speicher.
DB-Schema 8: songs.cloud_id verbindet Geraet und Server.
Nebenbei behoben (blockierte Build bzw. Tests):
- database.g.dart war veraltet — das Projekt liess sich nicht uebersetzen.
- metadataEdited wurde nirgends gesetzt/beachtet: von Hand korrigierte
Metadaten wurden vom naechsten Scan ueberschrieben. Jetzt in beiden
Scans respektiert; metadatenUebernahme() setzt die Markierung.
- song_detail_sheet_test.dart haengt beim Oeffnen des Modal-Sheets und
blockierte den gesamten Testlauf — vorerst uebersprungen (TODO im Code);
der Zweck wird von metadaten_uebernahme_test.dart abgedeckt.
Enthaelt ausserdem die bis dahin nicht committete Arbeit der Vorsitzung
(Musikerkennung/ACRCloud, MusicBrainz-Metadaten, MediaStore-Datentraeger).
267 Tests gruen (1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
- AutoScan: beim Zurueckkehren in die App wird die Zahl der Musikdateien
mit der Bibliothek verglichen; nur bei Abweichung laeuft ein Scan, und
hoechstens alle 5 Minuten. Gleich viele Loeschungen wie Neuzugaenge
bleiben unbemerkt - dafuer gibt es den Scan von Hand.
- ServerNeuheiten: meldet neu auf Navidrome aufgetauchte Alben im
Online-Tab. Der erste Abruf meldet bewusst nichts, sonst waere die ganze
Sammlung "neu"; er nutzt die ohnehin geladenen Alben statt eines zweiten
Serveraufrufs.
- Neu: MeloDb.countSongs(), countAndroidMediaStoreSongs().
189 Tests gruen, flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Neuer YouTube-Bereich im Online-Tab: Adresse einfuegen, herunterladen, der
Titel landet in Meine Musik.
- BakaAuth: Anmeldung gegen baka-net.de/auth, Token verschluesselt auf dem
Geraet. Der Server meldet Fehler mit HTTP 200 und Text im Rumpf, deshalb
wird der Inhalt geprueft statt nur der Statuscode.
- YtDownloadService: Proxy fragen, MP3 abholen, halbe Dateien aufraeumen.
Kuenstler nur aus "Kuenstler - Lied", sonst leer statt geraten.
- MediaStoreBridge (Kotlin): legt die Datei in Music/Melo ab und meldet sie
dem MediaStore. Noetig, weil der Android-Scan sonst beim naechsten Lauf
alles wieder als verschwunden markiert.
- SubTabs nach shared/ gezogen (jetzt in Meine Musik und Online genutzt).
177 Tests gruen, flutter analyze ohne Befund, Release-APK baut.
Der Weg ueber den echten Proxy ist noch nicht auf dem Geraet erprobt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Untere Navigation folgt jetzt den Referenzbildern:
Meine Musik / Online / Suchen / Favoriten. Der bisherige "Download"-Tab
heisst "Online" und bekommt als Naechstes den YouTube-Downloader.
In "Meine Musik" gibt es Unterreiter (Songs/Kuenstler/Alben) im Pillen-Stil
der Referenz. Sie wechseln nur den Inhalt statt einen Bildschirm zu oeffnen;
die Schnellzugriff-Kacheln "Kuenstler"/"Alben" entfallen dadurch, ebenso die
damit tot gewordenen ArtistsScreen/AlbumsScreen-Huellen.
158 Tests gruen, flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Sperrbildschirm und ReplayGain
- DB-Schema 6: lyrics + gain_db
- Songtext bevorzugt lokalen Tag, Server nur als Rueckfall; Song-ID-Bug
beim Lyrics-Aufruf behoben
- "Als Naechstes spielen" / "Zur Warteschlange hinzufuegen" ohne Eingriff
in Favoriten oder Wiedergabelisten
- songToMediaItem nimmt eine Cover-Vorgabe: Sperrbildschirm zeigt das
Kategorie-Cover
- ReplayGain: Tags werden gelesen, laute Titel abgesenkt, abschaltbar
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
- Meine Musik als Startbildschirm ohne Topbar: Kopfzeile (Einstellungen,
Suche, Musikerkennung), Schnellzugriffe, Shuffle + Sortieren, Liederliste
- Sortierung nach Zuletzt hinzugefuegt / Name (A-Z-#) / Wie oft abgespielt,
auf- und absteigend, pro Liste gespeichert (SortStore)
- Favoriten als eigener Tab mit gleicher Shuffle-/Sortier-Leiste
- Download-Tab uebernimmt den Navidrome-Server-Browser
- DB-Schema 3: playCount, gezaehlt beim Titelstart
- Theme auf #0B0B10 / #c0392b
- Scan-Aktionen aus der entfallenen Topbar jetzt in den Einstellungen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
Tiefe Debug-Runde bewiesen: Server, DB-Passwort und App-Token-Logik sind
korrekt (8/8-Repro gegen Live-Server) — abgelehnt wird ein falscher
Passwort-Wert vom Gerät. Damit das künftig sofort sichtbar ist:
- NavidromeService.letzterLoginFehler hält die Server-Meldung fest
- Login-Dialog zeigt sie statt generischem "Login fehlgeschlagen"
- Fehler-Log enthält die Passwort-Länge (nie das Passwort selbst)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRj3pupcigUi683vy2Wwnz
Diagnose-Logs werden zentral bereinigt, bevor sie an logs.baka-net.de gehen.
Lokaler debugPrint bleibt vollständig.
- Freitext-Scrubber (LoggerService.scrubText) für message + stackTrace:
maskiert user=/pw=/password=/token=/secret=/url= sowie Datei-Pfade,
nackte URLs, Bearer-Token, JWTs und lange Hex-IDs.
- stackTrace auf 8 Zeilen gekürzt (capFrames).
- deviceModel nur noch grobe Plattform (android/ios) statt Hersteller+Modell;
device_info_plus im Logger entfernt.
- Quelle entschärft: settings_screen loggte Navidrome-Login mit Klartext
user=/url= — jetzt nur neutraler Status inkl. pwLeer-Flag.
- Neue Tests: test/logger_service_test.dart (9). 79/79 grün, analyze sauber.
Opt-in/Einwilligung bleibt separater Folgeschritt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
- getPlaylists() + getPlaylistSongs() APIs für Subsonic-Playlist-Laden
- syncPlaylistsFromServer() in PlaylistService importiert alle Server-Playlisten als lokale
- Benennung mit 🌐 Präfix um Server-Playlisten zu kennzeichnen
- Settings-Button lädt alle Playlisten mit ihren Songs vom Server
- Zeigt Anzahl importierter Playlisten mit SnackBar
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- Bibliotheks-Statistik: Song-Anzahl + Gesamtspieldauer via watchSongs()
(reine Summier-/Format-Funktion in lib/settings/library_stats.dart,
unabhängig von Flutter/Plattform testbar)
- Berechtigungen: Musik-Berechtigung öffnet Systemeinstellungen über das
bereits vorhandene openMusicPermissionSettings()
- Über Melo: kurzer Name + Slogan, keine erfundene Versionsnummer
(package_info_plus ist keine Dependency)
- Keine Platzhalter für Profil/Cloud-Sync/Geräte-Verwaltung, da es noch
kein Account-/Cloud-System gibt (Phase 2)
- HomeShell nutzt SettingsScreen statt des Settings-Placeholders,
toter _Placeholder-Widget-Code entfernt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- MeloAudioHandler bekommt MeloDb per Konstruktor injiziert
- Positions-Timer (alle 5s) schreibt db.recordPlayback(songId, positionMs)
während aktiv abgespielt wird (songId aus mediaItem.extras['songId'])
- loadPlaylist liest db.lastPosition() für den Start-Song und seekt vor
dem play() an die gespeicherte Position, statt immer von vorne zu starten
- shouldResumeAt() als reine, testbare Funktion extrahiert: kein Resume bei
Position 0 oder innerhalb der letzten 3s einer bekannten Track-Dauer
- dispose() zum gefahrlosen Abbrechen des Positions-Timers in Tests ohne
vollen App-Lifecycle (super.stop() lässt sich in Widget-Tests wegen der
aktiven playbackState-Pipe nicht gefahrlos aufrufen)
- library_screen_test.dart: MeloAudioHandler-Konstruktion angepasst (db
jetzt Pflichtparameter) + handler.dispose() im Teardown
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Neuer QueueScreen: zeigt handler.queue, hebt aktuell spielenden Titel
hervor (fett/rot), Antippen springt per skipToQueueItem
- Entfernen und Umsortieren ohne Änderung an MeloAudioHandler: Queue wird
clientseitig neu berechnet und per loadPlaylist neu geladen; Position
wird nur wiederhergestellt, wenn der abgespielte Titel unverändert blieb
- Reine Queue-Logik (removeFromQueue/reorderQueue/newPlayingIndex) als
eigenständige, ohne AudioHandler testbare Funktionen extrahiert
- Warteschlangen-Button in der Now-Playing-AppBar verlinkt den neuen Screen
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Library screen now has a TabBar (Alle / Künstler / Alben) inside a
DefaultTabController; scan/permission/error banners stay above the
TabBarView so they remain visible regardless of the active sub-tab
- New lib/library/song_grouping.dart: pure, unit-tested functions
groupByArtist/groupByAlbum (null artist/album -> "Unbekannt" /
"Unbekanntes Album", alphabetically sorted) plus albumArtistLabel
("Verschiedene Interpreten" when an album mixes artists)
- New lib/library/artist_list.dart + album_list.dart: StreamBuilder on
MeloDb.watchSongs(), grouped client-side, tapping a row pushes a
SongList screen for that artist/album; album rows show a CoverImage
thumbnail from the first song's coverPath
- Existing "Alle" tab content (recently-added strip, full song list,
empty state) preserved unchanged, moved into _AllSongsTab
Genre browsing is intentionally out of scope: the Songs table has no
genre column and neither scanner extracts genre metadata — needs a
schema migration + scanner work in a future task.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
PlaylistDetailScreen zeigt die Songs einer Playlist (watchPlaylistSongs),
erlaubt Entfernen einzelner Songs und Umsortieren per Drag & Drop
(ReorderableListView.builder). Tippen auf einen Song spielt die Playlist
ab dieser Stelle. Leer-Zustand mit Hinweistext.
Da bestehende position-Werte nach Einfügereihenfolge lückenhaft sein
können, schreibt das Umsortieren nicht nur den verschobenen Song um,
sondern vergibt für die gesamte Playlist neue lückenlose Positionen
(0..N-1): MeloDb.reorderAllPlaylistSongs (in einer Transaktion) +
PlaylistService.reorderAll als dünner Wrapper.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- PlaylistsScreen: Favoriten-ListTile oben (watchFavorites, pusht FavoritesListScreen),
darunter alle Playlisten (watchPlaylists) mit Songanzahl, Löschen via Icon/Long-Press
+ Bestätigungsdialog
- CreatePlaylistDialog: Name-Eingabe, Erstellen-Button erst aktiv bei nicht-leerem Text,
gibt die neue Playlist-ID zurück
- Leer-Zustand "Noch keine Playlisten" nach Vorbild von library_screen.dart
- main.dart: Playlisten-Platzhalter durch PlaylistsScreen ersetzt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- songToMediaItem trägt jetzt die Song-UUID in extras['songId'], damit
Downstream-Code (Favoriten-Button, später Playback-History) den echten
Song wiederfinden kann statt nur die file-URI zu haben
- NowPlayingScreen: Schließen-Button entfernt (AppBar-leading), da der
Screen jetzt als eigener Tab läuft statt gepusht zu werden
- FavoriteButton für den aktuellen Song in die AppBar-Actions eingebaut,
reaktiv über einen verschachtelten StreamBuilder<MediaItem?>; ohne
gültige songId (z.B. nichts spielt) wird kein Button gerendert
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Filled heart when favorited, outline when not (reactive via watchIsFavorite)
- Wired into every song list tile (library, search, playlists)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Pre-commit analyze gate requires zero issues; this pre-existing
warning was blocking the schema commit.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Timer stops playback (pause) after 5/10/15/30/60 min. Wall-clock
deadline is self-correcting against OS throttling; 1Hz tick drives
the countdown UI. Cancelled in stop() so no timer resurrects a paused
player after teardown.
- sleep_timer.dart: isolated time logic, injectable clock for testing
- audio_handler.dart: sleepTimer field + cancel() in stop()
- now_playing_screen.dart: AppBar button + bottom sheet
- 10 unit tests via mocked clock
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Critical Blockers Fixed
- Remove playbackState.addError() which broke audio_service permanently
- Move error handling from audio_handler to song_list where UI context exists
- Display error in SnackBar instead of destroying playback stream
- Add escapeChar parameter to LIKE queries for correct wildcard handling
- Fixes broken search for titles with _ or % characters
- Add dismissScanError() method to allow closing error banner
- Previously banner couldn't be dismissed (missing notifyListeners())
## Improvements
- Remove dead livePaths list, use companions.isNotEmpty instead
- Add initial progress update to show total files when scan starts
- Add tooltip to now_playing_screen Zurück button
- Add proper error handling in song_list with async/await
## Testing
- Add regression test for CRITICAL-1: empty scan doesn't delete songs
- Add regression test for CRITICAL-2: tombstoned files restore on re-scan
- All 11 tests pass
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
## Critical Fixes
- Fix CRITICAL-1: Empty scans no longer delete entire library
- Use timestamp-based tombstone logic instead of path-based
- Only mark missing if livePaths is not empty
- Fix CRITICAL-2: Re-scanned files now return from deleted state
- Add deleted=false to SongsCompanion.insert in both scanners
## High Priority Fixes
- HIGH-1: Fix SQLite variable limit crash on large libraries
- Replace isNotIn() with timestamp comparison (O(1) not O(n))
- HIGH-2: Fix UI freezing during scan
- Throttle progress updates (every 50 files or end of scan)
- HIGH-3: Add error handling for scan failures
- Wrap scanFolders/scanAndroidMediaStore in try/catch
- Display scanError banner in LibraryScreen
- HIGH-4: Add error handling for playback failures
- Wrap loadPlaylist in try/catch
## Medium Priority Fixes
- MEDIUM-1: Replace ! with ?? to handle unknown ProcessingState
- MEDIUM-2: Escape LIKE wildcards in search queries
- MEDIUM-4: Display StreamBuilder errors instead of treating as empty
- MEDIUM-8: Add tooltips to all IconButtons for accessibility
- Add doc comments to public database APIs
## Testing
- Add regression tests for CRITICAL-1 and CRITICAL-2
- All 9 existing tests pass
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>