Files
Melo/.ui-design/reviews/melo_vollwertigkeit_20260823.md
T
Hermes (Server)andClaude Opus 5 e6d5f1c9ee Fuenf P0-Fehler behoben: Benachrichtigung, Offline, Cover, Warteschlange
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
2026-08-23 18:34:26 +02:00

18 KiB
Raw Blame History

Design- & Feature-Review: Melo vs. Apple Music / Spotify

Review-ID: melo_vollwertigkeit_20260823 Datum: 2026-08-23 Umfang: gesamte App (lib/, 56 Dateien, 14.721 Zeilen), Android-Manifest, Referenz-Bilder Fokus: Visual · Usability · Funktionsumfang · Code Werkzeuge: ui-ux-pro-max (design-system + flutter stack + pro-rules), ui-design:design-review

Verifizierter Ist-Zustand: flutter analyzeNo issues found. flutter test328 bestanden, 1 übersprungen (der bekannte Hänger aus BACKLOG.md).


Summary

Melo ist technisch überdurchschnittlich sauber: echtes Design-Token-System, WCAG-geprüfte Textfarben, Tombstone-fähiges Schema ab Tag 1, hohe Testabdeckung, deutschsprachige Erklär-Kommentare an genau den Stellen, wo früher Bugs saßen. Das ist deutlich besser als der Durchschnitt eines Hobby-Projekts.

Als vollwertige Musik-App im Sinne von Apple Music ist sie es aber noch nicht. Es fehlen nicht Details, sondern drei tragende Säulen: Alben als Browse-Achse, echtes Offline- Management und Cover-Art für Server-Titel. Dazu kommen zwei stille Fehler, die man der App nicht ansieht: der Offline-Schalter tut nichts, und ohne POST_NOTIFICATIONS erscheint auf Android 13+ keine Wiedergabe-Benachrichtigung.

Gefundene Punkte: 26 → 5 kritisch · 10 groß · 7 klein · 4 Vorschläge

Stand 2026-08-23, nach der Umsetzung: Die fünf P0-Punkte (K1K5) sind behoben, siehe CHANGELOG. G9 und G10 kamen bei der Umsetzung dazu und sind offen.


Kritische Punkte (P0)

K1 — Offline-Modus ist ein toter Schalter

Ort: lib/services/offline_mode.dart · lib/settings/settings_screen.dart:190 Kategorie: Usability / Korrektheit

OfflineMode speichert ein Bool in den SharedPreferences und ruft notifyListeners(). Das war's. Verifiziert per Volltextsuche: kein einziger Leser außerhalb von main.dart (Registrierung) und dem Settings-Schalter selbst. MeloAudioHandler, NavidromeService und SyncService fragen ihn nie ab.

Wirkung: Der Nutzer schaltet "Nur gecachte Lieder abspielen" ein, die App streamt trotzdem und verbraucht Mobilfunkdaten. Ein Versprechen, das die UI gibt und der Code bricht.

Empfehlung: OfflineMode in loadPlaylist auslesen. Ist er an, nur Titel mit Cache-Treffer oder lokalem Pfad in die Queue nehmen und die übersprungenen sichtbar machen ("3 Titel übersprungen — nicht offline verfügbar").


K2 — Keine POST_NOTIFICATIONS-Berechtigung

Ort: android/app/src/main/AndroidManifest.xml Kategorie: Plattform

Verifiziert im gemergten Manifest (build/app/intermediates/merged_manifests/release/.../AndroidManifest.xml): POST_NOTIFICATIONS ist nicht dabei. targetSdk folgt Flutter (≥ 33).

Wirkung: Auf Android 13+ zeigt das System die Media-Notification erst nach erteilter Laufzeit-Berechtigung. Ohne sie fehlt die zentrale Bedienfläche einer Musik-App im Alltag. Der Vordergrunddienst läuft, aber unsichtbar.

Empfehlung: Permission ins Manifest, beim ersten Start der Wiedergabe abfragen (permission_handler ist bereits als Abhängigkeit da).


K3 — Server-Titel haben nie ein Coverbild

Ort: lib/shared/cover.dart:38 · lib/downloads/downloads_screen.dart:309 Kategorie: Visual

CoverImage rendert ausschließlich artUri.scheme == 'file'. Für alles andere kommt der Noten-Platzhalter. Navidrome-Titel bekommen in _playAlbum/_playArtist gar kein artUri mitgegeben — obwohl NavidromeService.coverUrl (Zeile 265) existiert und SubsonicAlbum.coverId geparst wird.

Wirkung: Jeder Server-Titel ist in Liste, Mini-Player, Vollbild-Player, Notification und Sperrbildschirm ein graues Notensymbol. Das ist der augenfälligste Unterschied zu Apple Music — dort trägt das Cover die gesamte visuelle Identität.

Empfehlung: CoverImage um Image.network für http/https erweitern (mit cacheWidth, Fehler-Fallback auf den Platzhalter), und in beiden _play*-Methoden artUri: _nav.coverUrl(s.coverId) setzen. Bild-Cache lokal ablegen, damit es offline bleibt.


K4 — Alben fehlen als Browse-Achse

Ort: lib/library/song_grouping.dart · lib/library/my_music_screen.dart:88 Kategorie: Funktionsumfang

Songs.album ist eine Textspalte, aber es gibt weder eine Alben-Liste noch einen Album-Detail-Screen. song_grouping.dart kann nur groupByArtist. Die Unterreiter heißen Lieder / Kategorie / Künstler — im Referenz-Screenshot (ui-referenz/bugs/img_2d5761551808.jpg, das ist Melo selbst) stand dort noch Songs / Künstler / Alben. Die Achse ist also verlorengegangen.

Wirkung: Album ist neben Playlist die wichtigste Einheit, in der Menschen Musik denken. Ohne sie ist "Bibliothek" eine flache Titelliste — brauchbar bei 200 Titeln, unbrauchbar bei 5.000.

Empfehlung: groupByAlbum (analog zu groupByArtist, existierende _groupBy- Hilfsfunktion nutzt man mit), vierter Unterreiter, Album-Screen mit großem Cover, Künstler, Jahr, Titelzahl, Gesamtdauer und "Abspielen / Zufällig".


K5 — Warteschlange neu ordnen startet die Wiedergabe neu

Ort: lib/player/queue_screen.dart:87,103 · lib/player/audio_handler.dart:169 Kategorie: Usability / Korrektheit

Der Kommentar im Code ist ehrlich: der Handler bietet keine Live-Mutation, also ruft jedes Entfernen und jedes Verschieben loadPlaylist() auf. Das führt _player.setAudioSources(...) + play() + seek() aus.

Drei Folgen:

  1. Hörbarer Aussetzer bei jeder Queue-Änderung — bei Apple Music passiert das lautlos.
  2. loadPlaylist setzt _lastCountedIndex = null (Zeile 173) → der laufende Titel wird erneut als Wiedergabe gezählt. Die Sortierung "Wie oft abgespielt" wird durch Queue-Bedienung verfälscht.
  3. Bei Server-Titeln werden alle Cache-Treffer neu ermittelt (await in der Schleife).

Empfehlung: removeQueueItem/insertQueueItem auf _player.removeAudioSourceAt / moveAudioSource abbilden. just_audio kann das ohne Neuaufbau.


Große Punkte (P1)

G1 — Suche findet nur lokale Titel, nur flach

Ort: lib/library/search_screen.dart (59 Zeilen) db.searchSongs() per LIKE über Titel/Künstler/Album. Kein Server (search3.view wird nirgends aufgerufen), keine Playlisten, keine Kategorien, keine Gruppierung der Treffer nach Typ, keine letzten Suchen, keine Vorschläge, kein Entprellen. Apple Music gruppiert Ergebnisse in Titel/Alben/Künstler/Playlists und zeigt bei leerem Feld die Historie. Der ganze Tab ist heute ein Textfeld über einer Liste.

G2 — Navidrome ist im Download-Tab versteckt

Ort: lib/downloads/downloads_screen.dart:56 Der komplette Server-Browser ist ein Unterreiter von "Download". Ein Nutzer, der seine Server-Bibliothek hören will, sucht sie nicht unter "Download". Im Referenz-Screenshot hieß der Tab noch "Online" — treffender.

Dazu: Tippen auf ein Album spielt sofort ab. Es gibt keinen Album-Screen, keine Titelliste, keine Möglichkeit, einen einzelnen Server-Titel zu wählen. Nur 50 Alben, kein Nachladen, keine Server-Suche.

G3 — Server-Titel sind Bürger zweiter Klasse

Ort: lib/library/song_media.dart vs. lib/downloads/downloads_screen.dart:309 Ein MediaItem vom Server trägt nur navidromeIdkein songId. Damit gilt für Server-Titel: kein Favorit, keine Playlist-Zuordnung, kein lokaler Wiedergabezähler, keine Kategorie, kein Cover, kein Eintrag in der Historie.

Konkreter Fehler daraus: now_playing_screen.dart:45 gibt FavoriteButton(songId: item.extras?['songId'] ?? '') — bei Server-Titeln also den leeren String. FavoriteButton prüft nicht darauf, toggleFavorite('') legt eine Favoriten-Zeile mit leerer ID an. Das Herz füllt sich, in der Favoritenliste (INNER JOIN auf songs) taucht nichts auf. Falsche Rückmeldung an den Nutzer.

G4 — Kein gezielter Offline-Download

Ort: lib/services/cache_manager.dart · lib/player/audio_handler.dart:203 Gecacht wird nur, was zufällig abgespielt wurde. Es gibt kein "Album offline verfügbar machen", keine Anzeige, welche Titel offline sind, kein Verwalten pro Titel. CacheManager hat außerdem keine Größenbegrenzung und keine Verdrängung — nur "alles löschen". Der Cache wächst unbegrenzt. Alle Dateien werden .mp3 benannt, unabhängig vom tatsächlichen Codec (Navidrome liefert oft Opus/FLAC).

G5 — Kein "läuft gerade"-Zustand in der Liste

Ort: lib/library/song_list.dart:157 Die Referenz-UI zeigt den laufenden Titel farbig hervorgehoben mit animierten Balken. Melos SongList hat keinerlei Bezug zum mediaItem-Stream. Der Nutzer scrollt durch die Liste und sieht nicht, wo er gerade ist.

G6 — Zwei verschiedene Rot-Töne

Ort: 10 Vorkommen von Colors.redAccent (#FF5252), meist in settings_screen.dart und downloads_screen.dart Die Marke ist MeloTheme.red = #C0392B (gedecktes Ziegelrot). Colors.redAccent ist ein grelles Signalrot. Beide stehen im selben Screen nebeneinander. Insgesamt 58 hartcodierte Colors.*-Werte außerhalb des Themes, 18 davon allein in den Einstellungen — die sind visuell der schwächste Screen der App.

G7 — Emoji als Icon

Ort: downloads_screen.dart:234,247 (📀 Alben / 🎤 Künstler), settings_screen.dart:190 (📴 Offline-Modus), now_playing_screen.dart:362 (📝 Songtext) Verstößt gegen die erste Regel der Pre-Delivery-Checkliste von ui-ux-pro-max: Emoji rendern je nach Hersteller-Font unterschiedlich, lassen sich nicht einfärben, folgen keinem Design-Token und sind für Screenreader Unsinn. Material-Icons sind vorhanden (Icons.album, Icons.mic, Icons.cloud_off, Icons.lyrics).

G8 — Songtext ist ein statischer Textblock

Ort: lib/player/now_playing_screen.dart:309 Der Text wird als eine Wand angezeigt. Kein Zeilen-Sync (LRC), kein Mitlaufen, kein Antippen zum Springen. Navidromes getLyricsBySongId liefert synchronisierte Texte. Das ist bei Apple Music eine der meistgenutzten Funktionen.


G9 — Nach stop() merkt sich die App keine Positionen mehr (bei der P0-Umsetzung gefunden)

Ort: lib/player/audio_handler.dart:258 (stop) Kategorie: Korrektheit · Status: nicht behoben, außerhalb des P0-Auftrags

stop() ruft _positionRecordTimer.cancel(). Der Timer ist Timer.periodic und wird nur einmal im Konstruktor angelegt — einmal abgebrochen, kommt er für den Rest der App-Sitzung nicht zurück.

stop() löst nicht nur der Nutzer aus: das System ruft es beim Wegwischen der Benachrichtigung und über die Stop-Aktion auf. Danach speichert die App keine Wiedergabepositionen mehr und scrobbelt nicht mehr zum Server — bis zum Neustart, und ohne jeden Hinweis. „Fortsetzen an alter Stelle" hört damit still auf zu funktionieren.

Empfehlung: Den Timer in stop() nicht abbrechen, sondern in der Callback prüfen (if (!_player.playing) return; steht bereits drin — der Abbruch ist also gar nicht nötig). Abbrechen gehört allein in dispose().

Vorbestehend, nicht durch die P0-Änderungen entstanden: der alte QueueScreen rief in derselben Lage ebenfalls handler.stop() auf.


G10 — Server-Cover haben keinen dauerhaften Zwischenspeicher (Folge der K3-Umsetzung)

Ort: lib/shared/cover.dart:55 Kategorie: Performance · Status: offen, bewusst nicht in P0

Image.network legt nur im Arbeitsspeicher ab (Flutters ImageCache, ~100 MB, beim Beenden weg). Beim nächsten Kaltstart fragt jede sichtbare Zeile ihr Cover erneut beim Server an. Außerdem laufen diese Anfragen auch im Offline-Modus — sie schlagen dann fehl und fallen auf den Platzhalter zurück, kosten aber Versuche.

Ein dauerhafter Cache bräuchte entweder cached_network_image als neue Abhängigkeit oder eine Erweiterung des vorhandenen CacheManager auf Bilder. Beides ist eine Entscheidung, keine Fehlerbehebung — deshalb nicht im P0-Fix. Der Unterschied zu vorher bleibt trotzdem groß: bisher gab es überhaupt kein Server-Cover.


Kleine Punkte (P2)

# Punkt Ort
P1 AppBar der Einstellungen heißt "Settings" — einziges englisches Wort in einer deutschen App settings_screen.dart:65
P2 Kein Hero-Übergang Mini-Player → Vollbild (0 Treffer in lib/). Der Wechsel springt, statt das Cover wandern zu lassen mini_player.dart:70
P3 Kein Wischen am Mini-Player (nach unten = schließen, seitlich = nächster Titel) mini_player.dart
P4 Künstlerliste ist eine nackte ListTile-Liste ohne Bilder; kein Album-Zwischenschritt artist_list.dart:31
P5 Keine Reduced-Motion-Unterstützung (0 Treffer für disableAnimations), obwohl MeloMotion sauber getokent ist theme.dart:41
P6 Keine Querformat-/Tablet-Anpassung (LayoutBuilder nur in cover.dart, MediaQuery.size nur im Equalizer) app-weit
P7 Kein Alphabet-Schnellscroller und keine Mehrfachauswahl in langen Listen song_list.dart

Vorschläge (P3)

  • V1 — Farbe aus dem Cover ziehen. Apple Music und Spotify färben den Player nach dem Albumcover. In Flutter mit PaletteGenerator + Verlauf hinter dem Cover. Größter optischer Sprung pro Zeile Code.
  • V2 — Eigene Schrift. Die App nutzt System-Roboto. Die ui-ux-pro-max-Abfrage für "music entertainment" schlägt Righteous/Poppins vor; für Schwarz/Rot passt eher eine kräftige Grotesk (Outfit, Sora) für Überschriften bei Roboto/Inter im Fließtext.
  • V3 — YouTube-Share-Intent. Aktuell muss man die URL kopieren und einfügen. Ein ACTION_SEND-Intent-Filter im Manifest macht "Teilen → Melo" möglich. Sehr kleine Änderung, spürbarer Alltagsgewinn.
  • V4 — Android Auto / Assistant. Der MediaBrowserService ist im Manifest deklariert, aber MeloAudioHandler überschreibt getChildren/getMediaItem nicht — Browsen im Auto funktioniert also nicht. Kein Widget, keine Wear-Unterstützung.

Was gut ist (nicht ändern)

  • Design-Token-System (theme.dart): MeloSpace, MeloRadius, MeloMotion mit begründeten Werten. Die Textfarben sind nicht geschätzt, sondern per theme_kontrast_test.dart gegen 4,5:1 nachgerechnet. Das machen wenige.
  • Haarlinie statt Schatten auf Schwarz — richtige Entscheidung für OLED, deckt sich mit der "Dark Mode (OLED)"-Empfehlung des Style-Katalogs.
  • Datenmodell: UUID + updatedAtMs + deleted-Tombstone ab Schema 1, 9 saubere Migrationen. Sync war von Anfang an mitgedacht statt nachgerüstet.
  • Reine Funktionen ausgelagert (shouldResumeAt, playNextIndex, reorderQueue, planeSync, newPlayingIndex) — deshalb 328 Tests ohne Emulator.
  • Kommentare erklären das Warum, oft mit dem konkreten Bug, der dahinterstand (navidromeIdOf, cacheSchluessel, loadPlaylist). Genau die Stellen aus ui-referenz/bugs/ sind so dokumentiert.
  • Tooltips überall an Icon-Buttons → in Flutter automatisch Semantics-Label.

Priorisierter Plan

P0 — bevor irgendetwas Neues gebaut wird

  1. POST_NOTIFICATIONS ins Manifest + Laufzeit-Abfrage (K2, ~1 h)
  2. Offline-Modus wirklich verdrahten oder den Schalter entfernen (K1, ~3 h)
  3. CoverImage für http + artUri bei Server-Titeln setzen (K3, ~3 h)
  4. Queue ohne Neuladen mutieren, Doppelzählung beenden (K5, ~4 h)
  5. Leere songId in FavoriteButton abfangen (G3-Teil, ~30 min)

P1 — das macht sie "vollwertig"

  1. Alben-Achse: groupByAlbum, vierter Unterreiter, Album-Screen (K4, ~1 Tag)
  2. Suche umbauen: nach Typ gruppiert, Server einbeziehen, letzte Suchen (G1, ~1,5 Tage)
  3. Server-Bereich aus "Download" lösen → eigener Tab "Online" mit Album-Detail (G2, ~1,5 Tage)
  4. Offline-Download pro Album/Playlist + Cache-Grenze mit LRU (G4, ~1,5 Tage)
  5. "Läuft gerade"-Markierung in allen Listen (G5, ~3 h)

P2 — Politur, die man sofort sieht

  1. Colors.redAccentMeloTheme.red, Emoji → Material-Icons, "Settings" → "Einstellungen" (G6, G7, P1 — zusammen ~3 h)
  2. Hero-Übergang + Wischgesten am Mini-Player (P2, P3, ~4 h)
  3. Synchronisierter Songtext (LRC) (G8, ~1 Tag)
  4. Künstler- und Kategorieliste visuell aufwerten (P4, ~4 h)
  5. Reduced-Motion + Querformat (P5, P6, ~4 h)

P3 — danach

  1. Cover-Farbe im Player (V1)
  2. Eigene Schrift (V2)
  3. YouTube-Share-Intent (V3)
  4. Android Auto: getChildren implementieren (V4)

Feature-Matrix gegen Apple Music

Bereich Apple Music Melo Bewertung
Wiedergabe, Hintergrund, Sperrbildschirm da (Notification-Permission fehlt → K2)
Warteschlange ansehen/umordnen schwach — Neuladen bei jeder Änderung
Als Nächstes / Ans Ende da
Shuffle & Repeat da
Sleep-Timer da (Melo besser als Apple Music)
Equalizer ⚠️ nur System 5-Band + Bass + Virtualizer besser als Apple Music
Lautstärke-Angleich ReplayGain da
Fortsetzen an alter Stelle lokal + Server-Bookmark da
Favoriten da, aber nicht für Server-Titel
Playlisten anlegen/ordnen da
Alben durchsuchen fehlt
Künstler-Seite mit Alben ⚠️ nackte Titelliste schwach
Suche typgruppiert, serverweit ⚠️ lokal, flach schwach
Songtext zeilensynchron ⚠️ statischer Block schwach
Offline verfügbar machen gezielt ⚠️ nur Zufalls-Cache schwach
Cover-Art ⚠️ nur lokale Dateien schwach
Geräte-Sync der Bibliothek iCloud Melo-Cloud + Tombstones da
Scrobble / Wiedergabestatistik lokal + Subsonic da
Musikerkennung (Shazam) ACRCloud da
YouTube-Import Alleinstellung
Kategorien / eigene Tags Alleinstellung
Empfehlungen, Radio, Charts fehlt (bewusst? Melo hat keinen Katalog)
Teilen fehlt
Homescreen-Widget fehlt
Android Auto fehlt (Service da, Browsing nicht)
Cast / Sonos fehlt
Crossfade / Gapless fehlt

Zählung: 13 × da · 7 × schwach · 8 × fehlt · 3 × besser als Apple Music


Erzeugt mit ui-ux-pro-max 2.13.0 (design-system, --stack flutter, pro-rules) und ui-design:design-review. Alle Code-Stellen im Repo verifiziert, flutter analyze und flutter test zum Review-Zeitpunkt ausgeführt.