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:
@@ -50,6 +50,34 @@ class DownloadsListeScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _einzelnEntfernen(BuildContext context, Download eintrag) async {
|
||||
final dienst = context.read<DownloadService>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final ja = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Vom Gerät nehmen?'),
|
||||
content: Text('„${eintrag.titel}" wird vom Gerät genommen. '
|
||||
'Am Server bleibt der Titel unverändert.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Entfernen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ja != true) return;
|
||||
await dienst.entferne(eintrag.navidromeId);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('„${eintrag.titel}" entfernt')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final db = context.read<MeloDb>();
|
||||
@@ -102,9 +130,7 @@ class DownloadsListeScreen extends StatelessWidget {
|
||||
trailing: IconButton(
|
||||
tooltip: 'Vom Gerät nehmen',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () => context
|
||||
.read<DownloadService>()
|
||||
.entferne(eintrag.navidromeId),
|
||||
onPressed: () => _einzelnEntfernen(context, eintrag),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user