diff --git a/CHANGELOG.md b/CHANGELOG.md index 919b7d6..ce34284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,47 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). ## [Unreleased] +### 🐛 Gast-Zugang: Fixe aus dem Branch-Abschluss-Review (2026-08-26) + +- 🔓 **Gast-Modus war eine Einbahnstraße.** Einmal "Als Gast fortfahren" + angetippt, blieb `gast.hatToken` dauerhaft `true` (Token steckt im + Secure Storage, wird bei jedem App-Start wiederhergestellt) — und damit + war `_AnmeldeDialog`, der EINZIGE Einstieg in der App fĂŒr die + Baka-Anmeldung (auch fĂŒr den Melo-Cloud-Sync-Login!), unerreichbar. + Neu: `GastZugang.verwerfen()` verwirft den Token wieder, und ein + "Anmelden"-Knopf neben der Gast-Statuszeile (Download-Tab und + YT-Suche-Tab) ruft ihn auf. +- 🔀 **Ein alter Gast-Token hat den Auto-Login-RĂŒckfallweg fĂŒr + Server-User verdeckt.** `zugriffOk` prĂŒfte bisher den rohen + `gast.hatToken` statt `istGastModus` — ein GerĂ€t, das frĂŒher mal als + Gast lief und jetzt als Server-User mit fehlgeschlagenem Auto-Login + dasteht, bekam die Fehlermeldung dazu nie zu sehen und lud still ĂŒber + den (limitierten) Gast-Pfad. `istGastModus` wird jetzt vor `zugriffOk` + berechnet und dort statt des rohen Tokens verwendet. + (`lib/downloads/downloads_screen.dart`, + `lib/downloads/youtube_search_screen.dart`) +- ⏱ **Doppel-Tap auf "Als Gast fortfahren" konnte zwei Anfragen + gleichzeitig auslösen.** Beide Screens sperren den Button jetzt wĂ€hrend + `holeToken()` lĂ€uft (`_gastLaeuft`). +- 📝 **WidersprĂŒchlicher Hinweistext im YT-Suche-Tab behoben** — der Text + ĂŒber dem Gast-Button verlangte fĂ€lschlich eine Baka-Anmeldung. +- ⚠ **CHANGELOG ergĂ€nzt:** der Abschnitt zum Gast-Zugang unten macht jetzt + klar, dass die Server-Seite (`POST /api/guest-token`) noch fehlt. +- ✅ **Getestet:** 2 neue Regressionstests fĂŒrs "Anmelden"-Escape-Hatch + (`test/downloads/online_screen_test.dart`, + `test/downloads/youtube_search_screen_test.dart`), 2 bestehende Gast-Tests + im YT-Suche-Tab jetzt mit echtem `gast:`-Wiring in + `YtSearchService`/`YtDownloadService`. Volle Suite grĂŒn, `flutter + analyze` ohne Befund. + ### đŸ“șđŸ‘€ YouTube-Gast-Zugang: bewusste Wahl + Tages-ZĂ€hler (Download-Tab + YT-Suche-Tab) (2026-08-26) +- ⚠ **Braucht noch die Server-Seite.** Der neue Endpunkt `POST + /api/guest-token` und die Quota-Logik in `yt_proxy.py` existieren noch + nicht (separates Vorhaben, siehe + `docs/superpowers/specs/2026-08-26-youtube-guest-quota-design.md`). Bis + die Server-Seite steht, liefert der "Als Gast fortfahren"-Button + zuverlĂ€ssig einen 404-Fehler. - đŸ‘€ **Echte GĂ€ste (kein Baka-Konto, kein Melo-Server) sehen jetzt zwei Buttons** statt nur der Anmeldung: "Beim Baka-Konto anmelden" und neu "Als Gast fortfahren (5 Downloads/Tag)". Der Gast-Button holt sich per diff --git a/lib/downloads/downloads_screen.dart b/lib/downloads/downloads_screen.dart index 671d599..bc7063e 100644 --- a/lib/downloads/downloads_screen.dart +++ b/lib/downloads/downloads_screen.dart @@ -619,6 +619,9 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { /// Fehlertext, falls das Holen des Gast-Tokens scheitert. String? _gastFehler; + /// Verhindert Mehrfach-Anfragen bei schnellem Doppel-Antippen. + bool _gastLaeuft = false; + @override void initState() { super.initState(); @@ -690,11 +693,18 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { } Future _alsGastFortfahren() async { + if (_gastLaeuft) return; final gast = context.read(); - setState(() => _gastFehler = null); + setState(() { + _gastFehler = null; + _gastLaeuft = true; + }); final fehler = await gast.holeToken(); if (!mounted) return; - if (fehler != null) setState(() => _gastFehler = fehler); + setState(() { + _gastLaeuft = false; + if (fehler != null) _gastFehler = fehler; + }); } Future _herunterladen() async { @@ -755,10 +765,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { // Anmeldung nachweislich fehlgeschlagen ist, bleibt der manuelle Weg // als RĂŒckfalloption sichtbar — sonst gĂ€be es keinen Weg mehr zurĂŒck. // Ein Gast-Token zĂ€hlt ebenfalls als Zugang, nur mit Tages-Limit. + final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken; final zugriffOk = auth.istAngemeldet || (_serverUser && !_autoLoginFehlgeschlagen) || - gast.hatToken; - final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken; + istGastModus; return ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), @@ -794,7 +804,7 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { icon: const Icon(Icons.person_outline), label: const Text('Als Gast fortfahren (5 Downloads/Tag)', style: TextStyle(fontSize: 16)), - onPressed: _alsGastFortfahren, + onPressed: _gastLaeuft ? null : _alsGastFortfahren, ), ), if (_gastFehler != null) ...[ @@ -834,6 +844,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { style: const TextStyle(color: MeloTheme.text2, fontSize: 13), ), ), + TextButton( + onPressed: gast.verwerfen, + child: const Text('Anmelden'), + ), ], ), const SizedBox(height: 8), diff --git a/lib/downloads/youtube_search_screen.dart b/lib/downloads/youtube_search_screen.dart index 9e7b44a..f3191c5 100644 --- a/lib/downloads/youtube_search_screen.dart +++ b/lib/downloads/youtube_search_screen.dart @@ -38,6 +38,9 @@ class _YoutubeSearchScreenState extends State { /// Fehlertext, falls das Holen des Gast-Tokens scheitert. String? _gastFehler; + /// Verhindert Mehrfach-Anfragen bei schnellem Doppel-Antippen. + bool _gastLaeuft = false; + @override void initState() { super.initState(); @@ -56,11 +59,18 @@ class _YoutubeSearchScreenState extends State { } Future _alsGastFortfahren() async { + if (_gastLaeuft) return; final gast = context.read(); - setState(() => _gastFehler = null); + setState(() { + _gastFehler = null; + _gastLaeuft = true; + }); final fehler = await gast.holeToken(); if (!mounted) return; - if (fehler != null) setState(() => _gastFehler = fehler); + setState(() { + _gastLaeuft = false; + if (fehler != null) _gastFehler = fehler; + }); } @override @@ -81,10 +91,10 @@ class _YoutubeSearchScreenState extends State { final auth = context.watch(); final gast = context.watch(); final suche = context.watch(); + final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken; final zugriffOk = auth.istAngemeldet || (_serverUser && !_autoLoginFehlgeschlagen) || - gast.hatToken; - final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken; + istGastModus; return SafeArea( child: Column( @@ -116,8 +126,12 @@ class _YoutubeSearchScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Text( - 'Die Suche lĂ€uft ĂŒber den Baka-Server. DafĂŒr brauchst du ' - 'deine Baka-Anmeldung (siehe Download-Tab).', + _serverUser + ? 'Die Suche lĂ€uft ĂŒber den Baka-Server. DafĂŒr brauchst ' + 'du deine Baka-Anmeldung (siehe Download-Tab).' + : 'Die Suche lĂ€uft ĂŒber den Baka-Server. Melde dich mit ' + 'deinem Baka-Konto an (siehe Download-Tab) oder ' + 'nutze sie eingeschrĂ€nkt als Gast.', style: const TextStyle(color: MeloTheme.text2, fontSize: 13), ), ), @@ -131,7 +145,7 @@ class _YoutubeSearchScreenState extends State { icon: const Icon(Icons.person_outline), label: const Text('Als Gast fortfahren (5 Downloads/Tag)', style: TextStyle(fontSize: 16)), - onPressed: _alsGastFortfahren, + onPressed: _gastLaeuft ? null : _alsGastFortfahren, ), ), ), @@ -148,11 +162,21 @@ class _YoutubeSearchScreenState extends State { if (istGastModus) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - gast.verbleibend == null - ? 'Als Gast unterwegs (5 Downloads/Tag)' - : 'Als Gast unterwegs — noch ${gast.verbleibend} von 5 heute', - style: const TextStyle(color: MeloTheme.text2, fontSize: 13), + child: Row( + children: [ + Expanded( + child: Text( + gast.verbleibend == null + ? 'Als Gast unterwegs (5 Downloads/Tag)' + : 'Als Gast unterwegs — noch ${gast.verbleibend} von 5 heute', + style: const TextStyle(color: MeloTheme.text2, fontSize: 13), + ), + ), + TextButton( + onPressed: gast.verwerfen, + child: const Text('Anmelden'), + ), + ], ), ), if (suche.laeuft) const LinearProgressIndicator(), diff --git a/lib/services/gast_zugang.dart b/lib/services/gast_zugang.dart index 4df938a..5077395 100644 --- a/lib/services/gast_zugang.dart +++ b/lib/services/gast_zugang.dart @@ -79,4 +79,15 @@ class GastZugang extends ChangeNotifier { _verbleibend = n; notifyListeners(); } + + /// Verwirft den Gast-Zugang wieder — nötig, damit ein Gast doch noch zum + /// Baka-Konto wechseln kann. Ohne das wĂ€re `_AnmeldeDialog` (der einzige + /// Einstieg in der App fĂŒr Baka-Login, auch fĂŒr den Melo-Cloud-Sync) + /// dauerhaft unerreichbar, sobald einmal ein Gast-Token besteht. + Future verwerfen() async { + _token = null; + _verbleibend = null; + await _speicher.loeschen(_tokenKey); + notifyListeners(); + } } diff --git a/test/downloads/online_screen_test.dart b/test/downloads/online_screen_test.dart index d7219ad..8f68dd1 100644 --- a/test/downloads/online_screen_test.dart +++ b/test/downloads/online_screen_test.dart @@ -334,4 +334,32 @@ void main() { expect(find.text('Gast-Zugang fehlgeschlagen (500)'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsNothing); }); + + testWidgets( + 'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder die ' + 'Wahl', (tester) async { + final auth = BakaAuth( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher()); + final gast = GastZugang( + client: MockClient((_) async => + http.Response(jsonEncode({'guest_token': 'g-1'}), 200)), + speicher: _MemorySpeicher(), + ); + + await tester.pumpWidget( + _wrap(auth, db: db, categories: categories, gast: gast)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); + await tester.pumpAndSettle(); + expect(find.text('YouTube-Adresse'), findsOneWidget); + + await tester.tap(find.text('Anmelden')); + await tester.pumpAndSettle(); + + expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); + expect(find.text('YouTube-Adresse'), findsNothing); + expect(gast.hatToken, isFalse); + }); } diff --git a/test/downloads/youtube_search_screen_test.dart b/test/downloads/youtube_search_screen_test.dart index 2a827b2..12a1ece 100644 --- a/test/downloads/youtube_search_screen_test.dart +++ b/test/downloads/youtube_search_screen_test.dart @@ -92,7 +92,7 @@ void main() { db: db, auth: auth, search: search, download: download)); await tester.pumpAndSettle(); - expect(find.textContaining('Baka-Anmeldung'), findsOneWidget); + expect(find.textContaining('Baka-Server'), findsOneWidget); expect(find.text('Nach Musikvideos suchen'), findsOneWidget); await db.close(); @@ -191,11 +191,15 @@ void main() { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); - final search = YtSearchService(auth: auth); - final download = YtDownloadService(auth: auth); + final gast = GastZugang( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher(), + ); + final search = YtSearchService(auth: auth, gast: gast); + final download = YtDownloadService(auth: auth, gast: gast); await tester.pumpWidget(_wrap( - db: db, auth: auth, search: search, download: download)); + db: db, auth: auth, search: search, download: download, gast: gast)); await tester.pumpAndSettle(); expect( @@ -208,6 +212,34 @@ void main() { 'Als Gast fortfahren blendet den Baka-Hinweis aus und zeigt den ZĂ€hler', (tester) async { final db = MeloDb(NativeDatabase.memory()); + final auth = BakaAuth( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher()); + final gast = GastZugang( + client: MockClient((_) async => + http.Response(jsonEncode({'guest_token': 'g-1'}), 200)), + speicher: _MemorySpeicher(), + ); + final search = YtSearchService(auth: auth, gast: gast); + final download = YtDownloadService(auth: auth, gast: gast); + + await tester.pumpWidget(_wrap( + db: db, auth: auth, search: search, download: download, gast: gast)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); + await tester.pumpAndSettle(); + + expect(find.textContaining('Baka-Anmeldung'), findsNothing); + expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget); + + await db.close(); + }); + + testWidgets( + 'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder den ' + 'Hinweis', (tester) async { + final db = MeloDb(NativeDatabase.memory()); final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); @@ -225,10 +257,14 @@ void main() { await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); await tester.pumpAndSettle(); - - expect(find.textContaining('Baka-Anmeldung'), findsNothing); expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget); + await tester.tap(find.text('Anmelden')); + await tester.pumpAndSettle(); + + expect(find.textContaining('Baka-Server'), findsOneWidget); + expect(gast.hatToken, isFalse); + await db.close(); }); }