diff --git a/lib/downloads/downloads_screen.dart b/lib/downloads/downloads_screen.dart index fd2c99a..5326a71 100644 --- a/lib/downloads/downloads_screen.dart +++ b/lib/downloads/downloads_screen.dart @@ -679,9 +679,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { } Future _anmelden() async { + final nav = context.read().navidrome; await showDialog( context: context, - builder: (_) => const _AnmeldeDialog(), + builder: (_) => _AnmeldeDialog(benutzer: nav.benutzer), ); } @@ -869,14 +870,18 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { /// Fragt Benutzername und Passwort ab und gibt beides zurück. class _AnmeldeDialog extends StatefulWidget { - const _AnmeldeDialog(); + const _AnmeldeDialog({this.benutzer = ''}); + + /// Bereits bekannter Navidrome-Benutzername (leer, falls keiner bekannt) — + /// vorausgefüllt statt immer "Baka" zu zeigen, egal wer die App nutzt. + final String benutzer; @override State<_AnmeldeDialog> createState() => _AnmeldeDialogState(); } class _AnmeldeDialogState extends State<_AnmeldeDialog> { - final _benutzer = TextEditingController(text: 'Baka'); + late final _benutzer = TextEditingController(text: widget.benutzer); final _passwort = TextEditingController(); bool _sichtbar = false; bool _verbindet = false; diff --git a/lib/services/baka_auth.dart b/lib/services/baka_auth.dart index ecfa178..29c0cba 100644 --- a/lib/services/baka_auth.dart +++ b/lib/services/baka_auth.dart @@ -48,6 +48,12 @@ class BakaAuth extends ChangeNotifier { String? _token; String _benutzer = ''; + /// Laufender Login-Versuch aus [autoAnmelden] — solange er offen ist, + /// hängt sich ein überlappender Aufruf daran statt einen zweiten + /// Request loszuschicken (Race Condition: mehrere Tabs im IndexedStack + /// lösen beim App-Start gleichzeitig autoAnmelden aus). + Future? _laufenderAutoLogin; + bool get istAngemeldet => _token != null && _token!.isNotEmpty; String get benutzer => _benutzer; @@ -108,7 +114,9 @@ class BakaAuth extends ChangeNotifier { /// authentifizierten Aufruf ohnehin automatisch. Future autoAnmelden(String benutzer, String passwort) async { if (istAngemeldet || benutzer.isEmpty || passwort.isEmpty) return; - await anmelden(benutzer, passwort); + _laufenderAutoLogin ??= anmelden(benutzer, passwort) + .whenComplete(() => _laufenderAutoLogin = null); + await _laufenderAutoLogin; } Future abmelden() async { diff --git a/lib/settings/settings_screen.dart b/lib/settings/settings_screen.dart index 7aa9e24..5131c5f 100644 --- a/lib/settings/settings_screen.dart +++ b/lib/settings/settings_screen.dart @@ -346,7 +346,12 @@ class _SettingsScreenState extends State { void _zeigeNavidromeDialog() { final urlCtrl = TextEditingController(text: 'https://musik.baka-net.de'); final passCtrl = TextEditingController(); - String selectedUser = 'Baka'; + // Vorauswahl: der zuletzt bekannte Navidrome-Benutzer, falls er einer + // der drei Server-User ist — sonst Fallback "Baka". + const bekannteBenutzer = ['Baka', 'Tinker', 'Dustin']; + String selectedUser = bekannteBenutzer.contains(_navidrome.benutzer) + ? _navidrome.benutzer + : 'Baka'; bool verbindet = false; bool obscure = true; String? fehler; diff --git a/test/downloads/online_screen_test.dart b/test/downloads/online_screen_test.dart index 1d21203..d439c99 100644 --- a/test/downloads/online_screen_test.dart +++ b/test/downloads/online_screen_test.dart @@ -248,6 +248,26 @@ void main() { findsOneWidget); }); + testWidgets( + 'Der Anmelde-Dialog schlägt den bekannten Navidrome-Benutzer vor, ' + 'nicht immer "Baka"', (tester) async { + final auth = BakaAuth( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher()); + final nav = NavidromeService() + ..setCredentials('https://musik.baka-net.de', 'Tinker', 'falsch'); + + await tester.pumpWidget(_wrap(auth, + db: db, categories: categories, navidrome: nav)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Beim Baka-Konto anmelden')); + await tester.pumpAndSettle(); + + final feld = tester.widget(find.byType(TextField).first); + expect(feld.controller!.text, 'Tinker'); + }); + testWidgets( 'Server-User meldet sich im Hintergrund mit denselben Zugangsdaten ' 'beim Baka-Konto an', (tester) async { diff --git a/test/services/baka_auth_test.dart b/test/services/baka_auth_test.dart index 6be2650..97e5357 100644 --- a/test/services/baka_auth_test.dart +++ b/test/services/baka_auth_test.dart @@ -115,6 +115,30 @@ void main() { expect(auth.benutzer, 'Baka'); }); + test( + 'autoAnmelden löst bei gleichzeitigen Aufrufen nur EINEN HTTP-Request aus ' + '(Race Condition durch IndexedStack, das mehrere Tabs gleichzeitig baut)', + () async { + var aufrufe = 0; + final client = MockClient((_) async { + aufrufe++; + // Künstliche Verzögerung: simuliert, dass der erste Request noch + // nicht zurück ist, wenn der zweite Aufruf startet. + await Future.delayed(const Duration(milliseconds: 50)); + return http.Response(jsonEncode({'token': 'jwt-tinker'}), 200); + }); + final auth = BakaAuth(client: client, speicher: _MemorySpeicher()); + + await Future.wait([ + auth.autoAnmelden('Tinker', 'geheim'), + auth.autoAnmelden('Tinker', 'geheim'), + ]); + + expect(aufrufe, 1); + expect(auth.istAngemeldet, isTrue); + expect(auth.benutzer, 'Tinker'); + }); + test('autoAnmelden tut nichts ohne Zugangsdaten', () async { var aufrufe = 0; final client = MockClient((_) async {