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.
20 KiB
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 analyze → No issues found. flutter test → 328 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: die Album-Detailansicht (siehe die Korrektur
unter K4), 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. Erledigt: alle fünf P0-Punkte (K1–K5) sowie G1–G10. Details im CHANGELOG.
Zwei Teilforderungen aus G2 sind bewusst nicht umgesetzt: den Server-Bereich aus dem Download-Tab zu lösen widerspricht Commit
34d6d8e(„Auf Wunsch von Dustin"), und eine Server-Suche widerspricht der Vorgabe, dass die Suche rein lokal bleibt.Aus G3 bleibt der schwere Teil offen: Wiedergabezähler, Historie und Kategorien für reine Server-Titel bräuchten eine Zeile in der Bibliothek.
Nachtrag 2026-08-24: Die P2-Liste ist ebenfalls abgearbeitet (Hero-Übergang, Wischgesten, Künstlerseite mit Alben, Reduced Motion app-weit, Querformat/Tablet, Alphabet-Sprung, Mehrfachauswahl).
Nachtrag 2026-08-24 (zweiter): Auch die P3-Liste ist abgearbeitet — Cover-Farbe im Player, Teilen-Intent, Android Auto. Bei V2 (eigene Schrift) hat Dustin entschieden, bei der System-Schrift zu bleiben und stattdessen die Typografie zu schärfen; das ist umgesetzt.
Offen bleibt damit K4 in seiner korrigierten Fassung (Album-Detailansicht lokal, mit Tracknummer und Schema-Erweiterung — die Server-Seite davon ist mit G2 erledigt).
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 → korrigiert: Album-Detailansicht fehlt
Ort: lib/library/category_list.dart:12 · lib/library/categories.dart:26
Kategorie: Funktionsumfang
Korrektur vom 2026-08-23 (nach der P0-Runde). Die ursprüngliche Fassung dieses Punktes war falsch. Sie behauptete, die Alben-Achse sei „verlorengegangen". Sie wurde bewusst ersetzt:
category_list.dartdokumentiert das im Klassenkommentar, undkategorienAusTagssetzt den Album-Namen beim Scan als erste Kategorie jedes Titels. Der Reiter „Kategorie" ist der Alben-Reiter — nur allgemeiner, weil ein Titel mehreren Kategorien angehören darf. Der Fehler entstand, weil nursong_grouping.dartundmy_music_screen.dartgelesen wurden.
Was wirklich fehlt, ist nicht die Achse, sondern die Ansicht eines Albums als Album:
- Kein Album-Kopf.
_KategorieLiederScreenist eineAppBarüber einer Titelliste. Es fehlen großes Cover, Künstler, Jahr, Titelzahl, Gesamtdauer und ein „Abspielen / Zufällig"-Paar. Bei Apple Music trägt genau dieser Kopf die Identität eines Albums. - Falsche Reihenfolge. Innerhalb einer Kategorie wird nach Titel sortiert. Ein Album gehört in Tracknummer-Reihenfolge — sonst beginnt jedes Konzeptalbum mit dem Lied, das alphabetisch vorn steht.
- Die Tracknummer wird gar nicht gelesen. Verifiziert: weder
Songsnoch der Scan kennentrackNumber. Punkt 2 braucht also zuerst eine Schema-Erweiterung plus Auslesen des Tags. - Kein Weg vom Künstler zu seinen Alben.
_ArtistSongsScreenzeigt alle Titel flach.
Empfehlung (ersetzt die alte): Keinen vierten Reiter bauen — der würde
denselben Bestand doppelt zeigen. Stattdessen _KategorieLiederScreen zu
einem richtigen Detail-Screen ausbauen und die Tracknummer nachrüsten. Der
Aufwand liegt damit nicht bei „ein Reiter wie der Künstler-Reiter",
sondern bei Schema-Migration + Scan + Screen.
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:
- Hörbarer Aussetzer bei jeder Queue-Änderung — bei Apple Music passiert das lautlos.
loadPlaylistsetzt_lastCountedIndex = null(Zeile 173) → der laufende Titel wird erneut als Wiedergabe gezählt. Die Sortierung "Wie oft abgespielt" wird durch Queue-Bedienung verfälscht.- Bei Server-Titeln werden alle Cache-Treffer neu ermittelt (
awaitin 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 navidromeId — kein 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
MediaBrowserServiceist im Manifest deklariert, aberMeloAudioHandlerüberschreibtgetChildren/getMediaItemnicht — Browsen im Auto funktioniert also nicht. Kein Widget, keine Wear-Unterstützung.
Was gut ist (nicht ändern)
- Design-Token-System (
theme.dart):MeloSpace,MeloRadius,MeloMotionmit begründeten Werten. Die Textfarben sind nicht geschätzt, sondern pertheme_kontrast_test.dartgegen 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 ausui-referenz/bugs/sind so dokumentiert. - Tooltips überall an Icon-Buttons → in Flutter automatisch Semantics-Label.
Priorisierter Plan
P0 — bevor irgendetwas Neues gebaut wird
POST_NOTIFICATIONSins Manifest + Laufzeit-Abfrage (K2, ~1 h)- Offline-Modus wirklich verdrahten oder den Schalter entfernen (K1, ~3 h)
CoverImagefür http +artUribei Server-Titeln setzen (K3, ~3 h)- Queue ohne Neuladen mutieren, Doppelzählung beenden (K5, ~4 h)
- Leere
songIdinFavoriteButtonabfangen (G3-Teil, ~30 min)
P1 — das macht sie "vollwertig"
- Alben-Achse:
groupByAlbum, vierter Unterreiter, Album-Screen (K4, ~1 Tag) - Suche umbauen: nach Typ gruppiert, Server einbeziehen, letzte Suchen (G1, ~1,5 Tage)
- Server-Bereich aus "Download" lösen → eigener Tab "Online" mit Album-Detail (G2, ~1,5 Tage)
- Offline-Download pro Album/Playlist + Cache-Grenze mit LRU (G4, ~1,5 Tage)
- "Läuft gerade"-Markierung in allen Listen (G5, ~3 h)
P2 — Politur, die man sofort sieht
Colors.redAccent→MeloTheme.red, Emoji → Material-Icons, "Settings" → "Einstellungen" (G6, G7, P1 — zusammen ~3 h)- Hero-Übergang + Wischgesten am Mini-Player (P2, P3, ~4 h)
- Synchronisierter Songtext (LRC) (G8, ~1 Tag)
- Künstler- und Kategorieliste visuell aufwerten (P4, ~4 h)
- Reduced-Motion + Querformat (P5, P6, ~4 h)
P3 — danach
- Cover-Farbe im Player (V1)
- Eigene Schrift (V2)
- YouTube-Share-Intent (V3)
- Android Auto:
getChildrenimplementieren (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.