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.
This commit is contained in:
Hermes (Server)
2026-08-25 09:42:41 +02:00
parent 60c57577a3
commit 07acf77ba4
15 changed files with 511 additions and 202 deletions
+82 -25
View File
@@ -2,6 +2,8 @@ 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';
@@ -92,45 +94,100 @@ String kuenstlerUntertitel(int titel, int alben) =>
///
/// 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 {
class KuenstlerScreen extends StatefulWidget {
const KuenstlerScreen({super.key, required this.name, required this.songs});
final String name;
final List<Song> songs;
@override
State<KuenstlerScreen> createState() => _KuenstlerScreenState();
}
class _KuenstlerScreenState extends State<KuenstlerScreen> {
/// 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<String> _auswahl = const {};
bool get _waehltAus => _auswahl.isNotEmpty;
void _wechsleAuswahl(String songId) =>
setState(() => _auswahl = auswahlNach(_auswahl, songId));
void _beendeAuswahl() => setState(() => _auswahl = const {});
List<Song> get _gewaehlteSongs =>
[for (final s in widget.songs) if (_auswahl.contains(s.id)) s];
Future<void> _inWiedergabeliste() async {
await fuegeZuWiedergabelisteHinzu(context, _gewaehlteSongs);
if (mounted) _beendeAuswahl();
}
Future<void> _inWarteschlange() async {
await fuegeZuWarteschlangeHinzu(context, _gewaehlteSongs);
if (mounted) _beendeAuswahl();
}
@override
Widget build(BuildContext context) {
final alben = groupByAlbum(songs);
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;
// 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);
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');
}
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,
);
},
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),
);
},
),
),
);
}