Album-Ansicht, mitlaufender Songtext, Cover-Cache, Server-Favoriten

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
This commit is contained in:
Hermes (Server)
2026-08-23 21:53:31 +02:00
co-authored by Claude Opus 5
parent b42e045e67
commit 0d76d8cc58
16 changed files with 1194 additions and 142 deletions
+39 -18
View File
@@ -42,12 +42,14 @@ class SubsonicAlbum {
final String name;
final String? coverId;
final int songCount;
final String? kuenstler;
const SubsonicAlbum({
required this.id,
required this.name,
this.coverId,
this.songCount = 0,
this.kuenstler,
});
factory SubsonicAlbum.fromJson(Map<String, dynamic> j) => SubsonicAlbum(
@@ -55,6 +57,7 @@ class SubsonicAlbum {
name: j['name'] as String? ?? j['title'] as String? ?? 'Unbekannt',
coverId: j['coverArt'] as String?,
songCount: (j['songCount'] as int?) ?? 0,
kuenstler: j['artist'] as String?,
);
}
@@ -229,12 +232,15 @@ class NavidromeService {
}
}
Future<List<SubsonicAlbum>> getAlben({int anzahl = 50}) async {
/// [ab] überspringt die ersten Alben — damit lässt sich über die erste
/// Seite hinaus nachladen, statt bei 50 stehenzubleiben.
Future<List<SubsonicAlbum>> getAlben({int anzahl = 50, int ab = 0}) async {
try {
// getAlbumList2 = ID3-basiert (wie getArtists) — der Folder-Endpoint
// getAlbumList liefert bei tag-organisierten Navidrome-Libraries leer.
final r = await http
.get(_uri('getAlbumList2.view', {'type': 'newest', 'size': '$anzahl'}))
.get(_uri('getAlbumList2.view',
{'type': 'newest', 'size': '$anzahl', 'offset': '$ab'}))
.timeout(const Duration(seconds: 15));
if (r.statusCode != 200) throw NavidromeException('HTTP ${r.statusCode}');
return parseAlben(parseSubsonic(r.body));
@@ -244,16 +250,23 @@ class NavidromeService {
}
}
/// Die Titel eines Albums.
///
/// Wirft bei Fehlern weiter, statt eine leere Liste zu liefern: „keine
/// Titel" und „Server nicht erreichbar" sehen sonst gleich aus, und der
/// Nutzer bekäme keinen Wiederholen-Knopf zu sehen.
Future<List<SubsonicSong>> getSongs(String albumId) async {
try {
final r = await http.get(_uri('getAlbum.view', {'id': albumId})).timeout(const Duration(seconds: 15));
if (r.statusCode != 200) return [];
final data = jsonDecode(r.body);
final songs = data['subsonic-response']?['album']?['song'] as List? ?? [];
final r = await http
.get(_uri('getAlbum.view', {'id': albumId}))
.timeout(const Duration(seconds: 15));
if (r.statusCode != 200) throw NavidromeException('HTTP ${r.statusCode}');
final data = parseSubsonic(r.body);
final songs = data['album']?['song'] as List? ?? [];
return songs.map((j) => SubsonicSong.fromJson(j)).toList();
} catch (e) {
await logger.error('Navidrome getSongs Fehler: $e', e, StackTrace.current);
return [];
rethrow;
}
}
@@ -318,23 +331,31 @@ class NavidromeService {
}
}
/// Alle Titel eines Künstlers, Album für Album.
///
/// `getArtist` liefert die Alben **ohne** ihre Titel — das steht so in der
/// Subsonic-API. Vorher las diese Methode `album[].song`, was es dort nie
/// gibt: die Liste war immer leer, und der Künstler wirkte titellos.
/// Deshalb je Album ein `getAlbum` hinterher.
Future<List<SubsonicSong>> getArtistSongs(String artistId) async {
try {
final r = await http.get(_uri('getArtist.view', {'id': artistId})).timeout(const Duration(seconds: 15));
if (r.statusCode != 200) return [];
final data = jsonDecode(r.body);
final albums = data['subsonic-response']?['artist']?['album'] as List? ?? [];
final r = await http
.get(_uri('getArtist.view', {'id': artistId}))
.timeout(const Duration(seconds: 15));
if (r.statusCode != 200) throw NavidromeException('HTTP ${r.statusCode}');
final data = parseSubsonic(r.body);
final alben = data['artist']?['album'] as List? ?? [];
final songs = <SubsonicSong>[];
for (final album in albums) {
final albumSongs = album['song'] as List? ?? [];
for (final song in albumSongs) {
songs.add(SubsonicSong.fromJson(song));
}
for (final album in alben) {
final id = (album as Map)['id'] as String?;
if (id == null) continue;
songs.addAll(await getSongs(id));
}
return songs;
} catch (e) {
await logger.error('Navidrome getArtistSongs Fehler: $e', e, StackTrace.current);
return [];
await logger.error(
'Navidrome getArtistSongs Fehler: $e', e, StackTrace.current);
rethrow;
}
}