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
+67 -71
View File
@@ -7,7 +7,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../library/category_service.dart';
import '../library/database.dart';
import '../library/library_service.dart';
import '../library/song_media.dart';
import '../player/audio_handler.dart';
import '../services/baka_auth.dart';
import '../services/media_store.dart';
@@ -17,6 +16,7 @@ import '../services/server_neuheiten.dart';
import '../services/yt_download_service.dart';
import '../shared/sub_tabs.dart';
import 'download_einordnung.dart';
import 'server_titel_screen.dart';
import '../shared/theme.dart';
/// Download-Tab: YouTube-Downloader und der verbundene Navidrome-Server
@@ -74,9 +74,14 @@ class _ServerBrowserState extends State<_ServerBrowser> {
// Die Alben sind beim Prüfen schon geladen — kein zweiter Serveraufruf.
late final ServerNeuheiten _neuheiten =
ServerNeuheiten(holeAlben: () async => _albums);
static const _seite = 50;
List<SubsonicAlbum> _albums = [];
List<SubsonicArtist> _artists = [];
bool _loading = false;
bool _laedtNach = false;
/// Der Server hat weniger als eine volle Seite geliefert — dann ist Schluss.
bool _alleAlbenDa = false;
bool _showArtists = false;
String? _error;
@@ -99,10 +104,11 @@ class _ServerBrowserState extends State<_ServerBrowser> {
_error = null;
});
try {
final alben = await _nav.getAlben(anzahl: 50);
final alben = await _nav.getAlben(anzahl: _seite);
if (mounted) {
setState(() {
_albums = alben;
_alleAlbenDa = alben.length < _seite;
_loading = false;
});
// Erst jetzt prüfen — _albums ist gefüllt.
@@ -120,6 +126,29 @@ class _ServerBrowserState extends State<_ServerBrowser> {
}
}
/// Holt die nächste Seite Alben. Vorher blieb die Liste bei 50 stehen,
/// ohne dass irgendetwas darauf hinwies.
Future<void> _mehrAlben() async {
if (_laedtNach || _alleAlbenDa) return;
setState(() => _laedtNach = true);
try {
final weitere =
await _nav.getAlben(anzahl: _seite, ab: _albums.length);
if (!mounted) return;
setState(() {
_albums = [..._albums, ...weitere];
_alleAlbenDa = weitere.length < _seite;
_laedtNach = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _laedtNach = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Nachladen fehlgeschlagen: $e')),
);
}
}
Future<void> _loadArtists() async {
if (!_nav.istVerbunden) return;
setState(() {
@@ -270,15 +299,30 @@ class _ServerBrowserState extends State<_ServerBrowser> {
tooltip: 'Alles von ${artist.name} herunterladen',
holeTitel: () => _nav.getArtistSongs(artist.id),
),
onTap: () => _playArtist(artist),
onTap: () => _oeffneKuenstler(artist),
),
);
},
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: _albums.length,
itemCount: _albums.length + (_alleAlbenDa ? 0 : 1),
itemBuilder: (context, i) {
if (i == _albums.length) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: MeloSpace.md),
child: Center(
child: _laedtNach
? const CircularProgressIndicator(
color: MeloTheme.red)
: OutlinedButton(
onPressed: _mehrAlben,
child: const Text('Mehr Alben laden'),
),
),
);
}
final album = _albums[i];
return Card(
margin: const EdgeInsets.only(bottom: 8),
@@ -294,7 +338,7 @@ class _ServerBrowserState extends State<_ServerBrowser> {
tooltip: 'Album herunterladen',
holeTitel: () => _nav.getSongs(album.id),
),
onTap: () => _playAlbum(album),
onTap: () => _oeffneAlbum(album),
),
);
},
@@ -304,74 +348,26 @@ class _ServerBrowserState extends State<_ServerBrowser> {
);
}
Future<void> _playAlbum(SubsonicAlbum album) async {
final handler = context.read<MeloAudioHandler>();
try {
final songs = await _nav.getSongs(album.id);
if (songs.isEmpty) {
if (!mounted) {
return;
}
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
const SnackBar(content: Text('Album hat keine Songs')),
);
return;
}
final items = [for (final s in songs) subsonicToMediaItem(s, _nav)];
final uebersprungen = await handler.loadPlaylist(items);
if (mounted && uebersprungen > 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(offlineHinweis(uebersprungen))),
);
}
} on NichtsOfflineVerfuegbar catch (e) {
// Kein Fehler, sondern die gewählte Einstellung — deshalb ohne
// "Fehler:" davor, sonst sucht der Nutzer eine Störung.
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$e')),
);
} catch (e) {
if (!mounted) {
return;
}
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
SnackBar(content: Text('Fehler: $e')),
);
}
void _oeffneAlbum(SubsonicAlbum album) {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ServerTitelScreen(
titel: album.name,
untertitel: album.kuenstler,
coverId: album.coverId,
navidrome: _nav,
holeTitel: () => _nav.getSongs(album.id),
),
));
}
Future<void> _playArtist(SubsonicArtist artist) async {
final handler = context.read<MeloAudioHandler>();
try {
final songs = await _nav.getArtistSongs(artist.id);
if (songs.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Künstler hat keine Songs')),
);
return;
}
final items = [for (final s in songs) subsonicToMediaItem(s, _nav)];
final uebersprungen = await handler.loadPlaylist(items);
if (mounted && uebersprungen > 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(offlineHinweis(uebersprungen))),
);
}
} on NichtsOfflineVerfuegbar catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$e')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler: $e')),
);
}
void _oeffneKuenstler(SubsonicArtist artist) {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ServerTitelScreen(
titel: artist.name,
navidrome: _nav,
holeTitel: () => _nav.getArtistSongs(artist.id),
),
));
}
}