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
+51 -11
View File
@@ -464,7 +464,12 @@ class _LadeKnopfState extends State<_LadeKnopf> {
if (dienst.fehlgeschlagen > 0) {
teile.add('${dienst.fehlgeschlagen} fehlgeschlagen');
}
if (teile.isEmpty) return 'War schon heruntergeladen';
if (teile.isEmpty) {
// Kein Titel, kein Fehlschlag: entweder war wirklich schon alles da,
// oder der Download ist gar nicht erst gestartet (z.B. keine
// Verbindung zum Server) — dann steckt der Grund in dienst.fehler.
return dienst.fehler ?? 'War schon heruntergeladen';
}
return teile.join(' · ');
}
@@ -634,15 +639,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
}
Future<void> _anmelden() async {
final auth = context.read<BakaAuth>();
final daten = await showDialog<(String, String)>(
await showDialog(
context: context,
builder: (_) => const _AnmeldeDialog(),
);
if (daten == null || !mounted) return;
final fehler = await auth.anmelden(daten.$1, daten.$2);
if (!mounted) return;
setState(() => _meldung = fehler);
}
Future<void> _herunterladen() async {
@@ -785,7 +785,11 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
if (_meldung != null) ...[
const SizedBox(height: 16),
Text(_meldung!,
style: const TextStyle(color: MeloTheme.text2, fontSize: 13)),
style: TextStyle(
color: _meldung!.startsWith('')
? MeloTheme.text2
: MeloTheme.red,
fontSize: 13)),
],
],
);
@@ -804,6 +808,8 @@ class _AnmeldeDialogState extends State<_AnmeldeDialog> {
final _benutzer = TextEditingController(text: 'Baka');
final _passwort = TextEditingController();
bool _sichtbar = false;
bool _verbindet = false;
String? _fehler;
@override
void dispose() {
@@ -812,6 +818,24 @@ class _AnmeldeDialogState extends State<_AnmeldeDialog> {
super.dispose();
}
Future<void> _anmelden() async {
setState(() {
_verbindet = true;
_fehler = null;
});
final auth = context.read<BakaAuth>();
final fehler = await auth.anmelden(_benutzer.text.trim(), _passwort.text);
if (!mounted) return;
if (fehler == null) {
Navigator.pop(context);
return;
}
setState(() {
_verbindet = false;
_fehler = fehler;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
@@ -821,10 +845,12 @@ class _AnmeldeDialogState extends State<_AnmeldeDialog> {
children: [
TextField(
controller: _benutzer,
enabled: !_verbindet,
decoration: const InputDecoration(labelText: 'Benutzername'),
),
TextField(
controller: _passwort,
enabled: !_verbindet,
obscureText: !_sichtbar,
decoration: InputDecoration(
labelText: 'Passwort',
@@ -834,16 +860,30 @@ class _AnmeldeDialogState extends State<_AnmeldeDialog> {
),
),
),
if (_fehler != null) ...[
const SizedBox(height: 8),
Text(_fehler!,
style: const TextStyle(color: MeloTheme.red, fontSize: 12)),
],
if (_verbindet)
const Padding(
padding: EdgeInsets.only(top: 12),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: MeloTheme.red),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: _verbindet ? null : () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
FilledButton(
onPressed: () => Navigator.pop(
context, (_benutzer.text.trim(), _passwort.text)),
onPressed: _verbindet ? null : _anmelden,
child: const Text('Anmelden'),
),
],