Files
Melo/lib/shared/auswahl_leiste.dart
Hermes (Server) 07acf77ba4 UX-Simulation: Mehrfachauswahl überall, Baka-Login, Bestätigungen, Suche
7 Agenten sind die App als normaler Nutzer durchgegangen (Erststart,
Bibliothek, Download, Suche, Favoriten, Player, Einstellungen) und haben
39 Reibungspunkte gefunden. Erster Block:

- Mehrfachauswahl lief nur im Lieder-/Favoriten-Reiter, nicht in
  Kategorie-/Künstler-/Album-Ansichten — jetzt überall, per geteilter
  auswahl_leiste.dart
- Zwei unterschiedlich ausgestattete Favoriten-Listen (Tab vs. Playlisten)
  vereinheitlicht
- Baka-Login zeigt Ladezustand + Fehler im Dialog statt bis zu 15s stumm
  zu bleiben (wie der Navidrome-Login)
- Cache leeren, Navidrome-Abmelden, Download einzeln entfernen fragen jetzt
  nach
- Download-Fehlermeldung bei toter Serververbindung nennt den echten Grund
  statt "War schon heruntergeladen"
- YouTube-Fehlermeldungen sind jetzt rot statt neutralfarben
- Suche schließt die Tastatur nach Auswahl, nennt den Suchbegriff bei
  "Nichts gefunden", einzelne Verlaufseinträge sind entfernbar
- Icon-/Text-Korrekturen (Mikrofon-Icon fälschlich bei Speicherzugriff,
  "Online"-Tab existiert nicht, ReplayGain-Jargon)

537 Tests grün (vorher 533), flutter analyze ohne Befund.
2026-08-25 09:42:41 +02:00

162 lines
5.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../library/database.dart';
import '../library/category_service.dart';
import '../library/playlist_service.dart';
import '../library/song_media.dart';
import '../player/audio_handler.dart';
import '../playlists/create_playlist_dialog.dart';
import '../settings/app_settings.dart';
import 'auswahl.dart';
import 'theme.dart';
/// Kopfzeile im Auswahl-Modus: Anzahl und die beiden Stapel-Aktionen.
///
/// Gemeinsam für alle Songlisten, die Mehrfachauswahl anbieten — sie ersetzt
/// die sonst dort stehende Zeile, statt darunter zu erscheinen.
class AuswahlLeiste extends StatelessWidget {
const AuswahlLeiste({
super.key,
required this.anzahl,
required this.onAbbrechen,
required this.onWiedergabeliste,
required this.onWarteschlange,
});
final int anzahl;
final VoidCallback onAbbrechen;
final VoidCallback onWiedergabeliste;
final VoidCallback onWarteschlange;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(
MeloSpace.sm, MeloSpace.xs, MeloSpace.sm, MeloSpace.xs),
child: Row(
children: [
IconButton(
tooltip: 'Auswahl beenden',
icon: const Icon(Icons.close),
onPressed: onAbbrechen,
),
Expanded(
child: Text(
auswahlText(anzahl),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
IconButton(
tooltip: 'Zur Warteschlange hinzufügen',
icon: const Icon(Icons.queue),
onPressed: onWarteschlange,
),
IconButton(
tooltip: 'Zu Wiedergabeliste hinzufügen',
icon: const Icon(Icons.playlist_add),
onPressed: onWiedergabeliste,
),
],
),
);
}
}
/// Legt [gewaehlte] in eine (neue oder bestehende) Wiedergabeliste, mit
/// Rückmeldung über Neues/schon-Vorhandenes/Fehler.
Future<void> fuegeZuWiedergabelisteHinzu(
BuildContext context, List<Song> gewaehlte) async {
final db = context.read<MeloDb>();
final dienst = context.read<PlaylistService>();
final messenger = ScaffoldMessenger.of(context);
final ziel = await showModalBottomSheet<String>(
context: context,
builder: (sheetContext) => SafeArea(
child: StreamBuilder<List<Playlist>>(
stream: db.watchPlaylists(),
builder: (context, snapshot) {
final listen = snapshot.data ?? const <Playlist>[];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('Neue Wiedergabeliste'),
onTap: () async {
final neu = await showDialog<String?>(
context: sheetContext,
builder: (_) => const CreatePlaylistDialog(),
);
if (neu != null && sheetContext.mounted) {
Navigator.pop(sheetContext, neu);
}
},
),
for (final liste in listen)
ListTile(
leading: const Icon(Icons.playlist_play),
title: Text(liste.name),
onTap: () => Navigator.pop(sheetContext, liste.id),
),
],
);
},
),
),
);
if (ziel == null) return;
final vorhanden = await db.watchPlaylistSongs(ziel).first;
// Wer schon drin ist, bleibt, wo er ist. Ein erneutes Einfügen würde ihn
// wegen des Primärschlüssels nur verschieben und Lücken hinterlassen.
final bekannt = {for (final s in vorhanden) s.id};
var position = vorhanden.length;
var neu = 0;
String? fehler;
for (final song in gewaehlte) {
if (bekannt.contains(song.id)) continue;
try {
await dienst.addSongToPlaylist(ziel, song.id, position++);
neu++;
} catch (e) {
fehler = '$e';
break;
}
}
if (!context.mounted) return;
messenger.showSnackBar(SnackBar(
content: Text(playlistMeldung(
neu: neu,
schonDa: gewaehlte.length - neu,
fehler: fehler,
)),
));
}
/// Hängt [gewaehlte] ans Ende der Warteschlange, mit Rückmeldung, falls
/// nicht alle Titel angehängt werden konnten (z. B. offline).
Future<void> fuegeZuWarteschlangeHinzu(
BuildContext context, List<Song> gewaehlte) async {
final handler = context.read<MeloAudioHandler>();
final categories = context.read<CategoryService>();
final settings = context.read<AppSettings>();
final messenger = ScaffoldMessenger.of(context);
var angehaengt = 0;
for (final song in gewaehlte) {
final ok = await handler.addToQueue(songToMediaItem(
song,
cover: categories.coverFor(song,
groupByCategory: settings.groupCoversByCategory),
));
if (ok) angehaengt++;
}
if (!context.mounted) return;
messenger.showSnackBar(SnackBar(
content: Text(angehaengt == gewaehlte.length
? '$angehaengt Titel in der Warteschlange'
: '$angehaengt von ${gewaehlte.length} Titeln angehängt'),
));
}