Suche trennt nach Titel, Kuenstler, Kategorien und Wiedergabelisten
Der Suchen-Tab war 59 Zeilen: ein Textfeld ueber einer flachen
Liederliste, nur ueber Titel/Kuenstler/Album. Wer "Nightcore" eingab,
fand alle Titel dieses Kuenstlers untereinander, aber weder den
Kuenstler selbst noch eine gleichnamige Kategorie oder Liste.
- Vier Abschnitte mit Ueberschrift und Trefferzahl. Alben stecken in
"Kategorien" — der Album-Name ist in Melo die erste Kategorie eines
Titels, ein eigener Abschnitt waere dieselbe Liste zweimal.
- Treffer am Wortanfang zuerst, dann alphabetisch.
- Entprellt (250 ms); vorher baute jeder Tastendruck alles neu auf.
- Letzte acht Suchen, gemerkt beim Abschicken, mit Loeschen-Knopf.
- Der Tab folgt jetzt dem Aufbau der uebrigen Tabs (war ein Scaffold
mit eigener AppBar innerhalb eines Tabs).
- Bewusst rein lokal, ohne Server-Abfrage.
Herausgeloest, weil es sonst eine dritte Kopie gegeben haette:
TitelListenScreen ("Ueberschrift + Liederliste", stand zweimal wortgleich
im Baum) und SongZeile (die Suche setzt einzelne Zeilen in ihre
Abschnitte; eine ganze SongList waere dort verschachteltes Scrollen).
Aus dem Code-Review nachgebessert: Suche und Zuordnung benutzen dieselbe
Normalform, sonst zeigte ein Treffer "0 Titel" und oeffnete eine leere
Liste; Kategorien ohne lebende Titel fallen weg; alle Abschnitte sind
gedeckelt; die Drift-Stroeme werden nicht mehr je Rebuild neu abonniert;
das Loeschkreuz erscheint sofort statt nach 250 ms.
Tot geworden und entfernt: MeloDb.searchSongs samt Test.
396 Tests gruen (vorher 360), 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:
co-authored by
Claude Opus 5
parent
d15867d962
commit
ca28264c0e
+92
-64
@@ -18,8 +18,47 @@ class SongList extends StatelessWidget {
|
||||
const SongList(this.songs, {super.key});
|
||||
final List<Song> songs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: songs.length,
|
||||
itemBuilder: (context, i) => SongZeile(
|
||||
song: songs[i],
|
||||
warteschlange: songs,
|
||||
index: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Zeile der Songliste: Cover, Titel, Untertitel, Herz und Menü.
|
||||
///
|
||||
/// Eigenständig, weil die Suche einzelne Zeilen in ihre Abschnitte setzt und
|
||||
/// eine ganze [SongList] dort eine verschachtelte Scroll-Fläche wäre.
|
||||
///
|
||||
/// [warteschlange] ist die Liste, die beim Antippen geladen wird — in der
|
||||
/// Bibliothek die ganze Liste, in der Suche der Titel-Abschnitt.
|
||||
class SongZeile extends StatelessWidget {
|
||||
const SongZeile({
|
||||
super.key,
|
||||
required this.song,
|
||||
required this.warteschlange,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final Song song;
|
||||
final List<Song> warteschlange;
|
||||
final int index;
|
||||
|
||||
/// "Künstler | Kategorie1 · Kategorie2" — wie im UI-Entwurf.
|
||||
static String _subtitle(Song song, List<String> categories) {
|
||||
final artist = song.artist ?? 'Unbekannt';
|
||||
if (categories.isEmpty) return artist;
|
||||
return '$artist | ${categories.join(' · ')}';
|
||||
}
|
||||
|
||||
/// Öffnet ein Bottom-Sheet mit allen Playlisten, um [song] hinzuzufügen.
|
||||
Future<void> _showAddToPlaylist(BuildContext context, Song song) async {
|
||||
Future<void> _showAddToPlaylist(BuildContext context) async {
|
||||
final db = context.read<MeloDb>();
|
||||
final service = context.read<PlaylistService>();
|
||||
await showModalBottomSheet(
|
||||
@@ -75,20 +114,13 @@ class SongList extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// "Künstler | Kategorie1 · Kategorie2" — wie im UI-Entwurf.
|
||||
static String _subtitle(Song song, List<String> categories) {
|
||||
final artist = song.artist ?? 'Unbekannt';
|
||||
if (categories.isEmpty) return artist;
|
||||
return '$artist | ${categories.join(' · ')}';
|
||||
}
|
||||
|
||||
/// Menü hinter dem Drei-Punkte-Symbol einer Songzeile.
|
||||
///
|
||||
/// Die Offline-Meldungen greifen hier heute nie: diese Liste zeigt nur
|
||||
/// Titel der Bibliothek, und die liegen als Datei vor. Sie stehen trotzdem
|
||||
/// da, weil die Alternative — den Rückgabewert wegwerfen und "hinzugefügt"
|
||||
/// melden — still lügen würde, sobald ein Server-Titel hier ankommt.
|
||||
Future<void> _showMenu(BuildContext context, Song song) async {
|
||||
Future<void> _showMenu(BuildContext context) async {
|
||||
final handler = context.read<MeloAudioHandler>();
|
||||
final categories = context.read<CategoryService>();
|
||||
final settings = context.read<AppSettings>();
|
||||
@@ -119,9 +151,8 @@ class SongList extends StatelessWidget {
|
||||
Navigator.pop(sheetContext);
|
||||
final ok = await handler.playNext(item());
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(ok
|
||||
? 'Läuft als Nächstes'
|
||||
: nichtOfflineHinweis)));
|
||||
content: Text(
|
||||
ok ? 'Läuft als Nächstes' : nichtOfflineHinweis)));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -143,7 +174,7 @@ class SongList extends StatelessWidget {
|
||||
title: const Text('Zu Wiedergabeliste hinzufügen'),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showAddToPlaylist(context, song);
|
||||
_showAddToPlaylist(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -154,60 +185,57 @@ class SongList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final handler = context.read<MeloAudioHandler>();
|
||||
// Der Handler wird erst beim Antippen gebraucht und deshalb auch erst
|
||||
// dann gelesen: so lässt sich eine Zeile darstellen, ohne dass ein
|
||||
// laufender Player bereitstehen muss.
|
||||
final categories = context.watch<CategoryService>();
|
||||
final settings = context.watch<AppSettings>();
|
||||
return ListView.builder(
|
||||
itemCount: songs.length,
|
||||
itemBuilder: (context, i) {
|
||||
final s = songs[i];
|
||||
final cover = categories.coverFor(s,
|
||||
groupByCategory: settings.groupCoversByCategory);
|
||||
return ListTile(
|
||||
leading: CoverImage(
|
||||
artUri: cover != null ? Uri.file(cover) : null,
|
||||
size: 48,
|
||||
radius: 6,
|
||||
final cover = categories.coverFor(song,
|
||||
groupByCategory: settings.groupCoversByCategory);
|
||||
return ListTile(
|
||||
leading: CoverImage(
|
||||
artUri: cover != null ? Uri.file(cover) : null,
|
||||
size: 48,
|
||||
radius: 6,
|
||||
),
|
||||
title: Text(song.title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
_subtitle(song, categories.of(song.id)),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FavoriteButton(songId: song.id),
|
||||
IconButton(
|
||||
tooltip: 'Mehr',
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () => _showMenu(context),
|
||||
),
|
||||
title: Text(s.title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
_subtitle(s, categories.of(s.id)),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FavoriteButton(songId: s.id),
|
||||
IconButton(
|
||||
tooltip: 'Mehr',
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () => _showMenu(context, s),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final uebersprungen = await playSongs(
|
||||
handler,
|
||||
songs,
|
||||
i,
|
||||
coverOf: (song) => categories.coverFor(song,
|
||||
groupByCategory: settings.groupCoversByCategory),
|
||||
);
|
||||
if (uebersprungen > 0) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(offlineHinweis(uebersprungen))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Wiedergabe fehlgeschlagen: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final handler = context.read<MeloAudioHandler>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final uebersprungen = await playSongs(
|
||||
handler,
|
||||
warteschlange,
|
||||
index,
|
||||
coverOf: (s) => categories.coverFor(s,
|
||||
groupByCategory: settings.groupCoversByCategory),
|
||||
);
|
||||
if (uebersprungen > 0) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(offlineHinweis(uebersprungen))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Wiedergabe fehlgeschlagen: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user