import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../settings/app_settings.dart'; import '../shared/auswahl.dart'; import '../shared/auswahl_leiste.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 createState() => _ArtistListScreenState(); } class _ArtistListScreenState extends State { // 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> _songs = context.read().watchSongs(); @override Widget build(BuildContext context) { return StreamBuilder>( 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 songs; @override Widget build(BuildContext context) { final kategorien = context.watch(); final einstellungen = context.watch(); 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 StatefulWidget { const KuenstlerScreen({super.key, required this.name, required this.songs}); final String name; final List songs; @override State createState() => _KuenstlerScreenState(); } class _KuenstlerScreenState extends State { /// Song-IDs der gerade ausgewählten Titel in „Alle Titel" — leer heißt: /// kein Auswahl-Modus. Wie in SortableSongList, damit langes Drücken auf /// einer Songzeile überall in der App dasselbe tut. Set _auswahl = const {}; bool get _waehltAus => _auswahl.isNotEmpty; void _wechsleAuswahl(String songId) => setState(() => _auswahl = auswahlNach(_auswahl, songId)); void _beendeAuswahl() => setState(() => _auswahl = const {}); List get _gewaehlteSongs => [for (final s in widget.songs) if (_auswahl.contains(s.id)) s]; Future _inWiedergabeliste() async { await fuegeZuWiedergabelisteHinzu(context, _gewaehlteSongs); if (mounted) _beendeAuswahl(); } Future _inWarteschlange() async { await fuegeZuWarteschlangeHinzu(context, _gewaehlteSongs); if (mounted) _beendeAuswahl(); } @override Widget build(BuildContext context) { final alben = groupByAlbum(widget.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; return PopScope( // Zurück beendet zuerst die Auswahl, wie in SortableSongList — sonst // verlöre man die Auswahl mit derselben Geste, die überall sonst // „einen Schritt zurück" bedeutet. canPop: !_waehltAus, onPopInvokedWithResult: (didPop, _) { if (!didPop && _waehltAus) _beendeAuswahl(); }, child: Scaffold( appBar: _waehltAus ? AppBar( automaticallyImplyLeading: false, titleSpacing: 0, title: AuswahlLeiste( anzahl: _auswahl.length, onAbbrechen: _beendeAuswahl, onWiedergabeliste: _inWiedergabeliste, onWarteschlange: _inWarteschlange, ), ) : AppBar(title: Text(widget.name)), // 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. body: ListView.builder( itemCount: vorspann + widget.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; final song = widget.songs[index]; return SongZeile( song: song, warteschlange: widget.songs, index: index, auswahlModus: _waehltAus, ausgewaehlt: _auswahl.contains(song.id), onAuswahlWechsel: () => _wechsleAuswahl(song.id), ); }, ), ), ); } } class _AlbumZeile extends StatelessWidget { const _AlbumZeile({required this.name, required this.songs}); final String name; final List songs; @override Widget build(BuildContext context) { final kategorien = context.watch(); final einstellungen = context.watch(); 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), ); } }