Files
Melo/lib/library/artist_list.dart
T
Hermes (Server)andClaude Opus 5 490e73ce41 Politur: Wischen, wandernde Cover, Alphabet-Sprung, Mehrfachauswahl
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
2026-08-24 15:09:46 +02:00

180 lines
6.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../settings/app_settings.dart';
import '../shared/cover.dart';
import '../shared/theme.dart';
import '../shared/titel_listen_screen.dart';
import 'category_service.dart';
import 'database.dart';
import 'song_grouping.dart';
import 'song_list.dart';
/// Bibliotheks-Tab: Künstler-Übersicht, gruppiert aus [MeloDb.watchSongs].
class ArtistListScreen extends StatefulWidget {
const ArtistListScreen({super.key});
@override
State<ArtistListScreen> createState() => _ArtistListScreenState();
}
class _ArtistListScreenState extends State<ArtistListScreen> {
// Einmal anlegen, nicht in jedem build: die Zeilen hängen an
// CategoryService, und ein neuer Strom je Meldung würde die Abfrage
// während eines Scans laufend neu ausführen.
late final Stream<List<Song>> _songs = context.read<MeloDb>().watchSongs();
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Song>>(
stream: _songs,
builder: (context, snapshot) {
final songs = snapshot.data ?? const [];
if (songs.isEmpty) {
return const Center(
child: Text('Keine Künstler',
style: TextStyle(color: MeloTheme.text2)),
);
}
final grouped = groupByArtist(songs);
final artists = grouped.keys.toList();
return ListView.builder(
itemCount: artists.length,
itemBuilder: (context, i) {
final artist = artists[i];
return _KuenstlerZeile(name: artist, songs: grouped[artist]!);
},
);
},
);
}
}
/// Eine Künstlerzeile. Eigenes Widget, damit nur sie an [CategoryService]
/// hängt und nicht der Bildschirm mit seinem Datenbank-Strom.
class _KuenstlerZeile extends StatelessWidget {
const _KuenstlerZeile({required this.name, required this.songs});
final String name;
final List<Song> songs;
@override
Widget build(BuildContext context) {
final kategorien = context.watch<CategoryService>();
final einstellungen = context.watch<AppSettings>();
final cover = kategorien.coverFor(songs.first,
groupByCategory: einstellungen.groupCoversByCategory);
return ListTile(
// Ein Bild statt einer leeren Fläche: die Liste war reiner Text und
// dadurch schwer zu überfliegen.
leading: CoverImage(
artUri: cover != null ? Uri.file(cover) : null,
size: 48,
radius: 6,
),
title: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle:
Text(kuenstlerUntertitel(songs.length, groupByAlbum(songs).length)),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => KuenstlerScreen(name: name, songs: songs),
)),
);
}
}
/// "12 Titel · 3 Alben" — die Albenzahl nur, wenn es mehr als eines ist.
/// „Titel" hat im Deutschen keine eigene Mehrzahl, deshalb ohne Fallunterscheidung.
String kuenstlerUntertitel(int titel, int alben) =>
alben > 1 ? '$titel Titel · $alben Alben' : '$titel Titel';
/// Die Seite eines Künstlers: seine Alben, darunter alle Titel.
///
/// Vorher führte ein Künstler direkt auf eine flache Titelliste — bei einem
/// Künstler mit acht Alben stand alles ungeordnet untereinander.
class KuenstlerScreen extends StatelessWidget {
const KuenstlerScreen({super.key, required this.name, required this.songs});
final String name;
final List<Song> songs;
@override
Widget build(BuildContext context) {
final alben = groupByAlbum(songs);
// Bei einem einzigen Album wäre die Zwischenstufe nur ein Klick mehr.
final zeigeAlben = alben.length > 1;
final albumListe = zeigeAlben ? alben.entries.toList() : const [];
// Kopf „Alben" + Alben + Trenner + Kopf „Alle Titel"
final vorspann = zeigeAlben ? albumListe.length + 3 : 0;
// Bewusst `builder` und nicht `ListView(children: …)`: jede Songzeile
// hört auf den Player und öffnet für ihr Herz eine eigene Abfrage. Bei
// einem Künstler mit 300 Titeln wären das 300 Abos beim Öffnen der Seite.
return Scaffold(
appBar: AppBar(title: Text(name)),
body: ListView.builder(
itemCount: vorspann + songs.length,
itemBuilder: (context, i) {
if (zeigeAlben) {
if (i == 0) return const _Kopf('Alben');
if (i <= albumListe.length) {
final eintrag = albumListe[i - 1];
return _AlbumZeile(name: eintrag.key, songs: eintrag.value);
}
if (i == albumListe.length + 1) return const Divider(height: 1);
if (i == albumListe.length + 2) return const _Kopf('Alle Titel');
}
final index = i - vorspann;
return SongZeile(
song: songs[index],
warteschlange: songs,
index: index,
);
},
),
);
}
}
class _AlbumZeile extends StatelessWidget {
const _AlbumZeile({required this.name, required this.songs});
final String name;
final List<Song> songs;
@override
Widget build(BuildContext context) {
final kategorien = context.watch<CategoryService>();
final einstellungen = context.watch<AppSettings>();
final cover = kategorien.coverFor(songs.first,
groupByCategory: einstellungen.groupCoversByCategory);
return ListTile(
leading: CoverImage(
artUri: cover != null ? Uri.file(cover) : null,
size: 48,
radius: 6,
),
title: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text('${songs.length} Titel'),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => TitelListenScreen(titel: name, songs: songs),
)),
);
}
}
class _Kopf extends StatelessWidget {
const _Kopf(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(
MeloSpace.md, MeloSpace.md, MeloSpace.md, MeloSpace.xs),
child: Text(text.toUpperCase(),
style: Theme.of(context).textTheme.labelLarge),
);
}
}