diff --git a/lib/downloads/downloads_screen.dart b/lib/downloads/downloads_screen.dart index fd2c99a..671d599 100644 --- a/lib/downloads/downloads_screen.dart +++ b/lib/downloads/downloads_screen.dart @@ -11,6 +11,7 @@ import '../player/audio_handler.dart'; import '../services/baka_auth.dart'; import '../services/media_store.dart'; import '../services/download_service.dart'; +import '../services/gast_zugang.dart'; import '../services/navidrome_service.dart'; import '../services/server_neuheiten.dart'; import '../services/yt_download_service.dart'; @@ -615,6 +616,9 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { /// sichtbar, sonst käme so ein User nie mehr an den Dialog heran. bool _autoLoginFehlgeschlagen = false; + /// Fehlertext, falls das Holen des Gast-Tokens scheitert. + String? _gastFehler; + @override void initState() { super.initState(); @@ -685,6 +689,14 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { ); } + Future _alsGastFortfahren() async { + final gast = context.read(); + setState(() => _gastFehler = null); + final fehler = await gast.holeToken(); + if (!mounted) return; + if (fehler != null) setState(() => _gastFehler = fehler); + } + Future _herunterladen() async { final dienst = context.read(); final lib = context.read(); @@ -736,13 +748,17 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { @override Widget build(BuildContext context) { final auth = context.watch(); + final gast = context.watch(); final dienst = context.watch(); // Server-User gelten sofort als berechtigt — die Anmeldung passiert für // sie im Hintergrund (_pruefeServerUser). Nur wenn die automatische // Anmeldung nachweislich fehlgeschlagen ist, bleibt der manuelle Weg // als Rückfalloption sichtbar — sonst gäbe es keinen Weg mehr zurück. - final zugriffOk = - auth.istAngemeldet || (_serverUser && !_autoLoginFehlgeschlagen); + // Ein Gast-Token zählt ebenfalls als Zugang, nur mit Tages-Limit. + final zugriffOk = auth.istAngemeldet || + (_serverUser && !_autoLoginFehlgeschlagen) || + gast.hatToken; + final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken; return ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), @@ -769,8 +785,26 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { onPressed: _anmelden, ), ), - ] else ...[ if (!_serverUser) ...[ + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: MeloTheme.minTouchTarget + 4, + child: OutlinedButton.icon( + icon: const Icon(Icons.person_outline), + label: const Text('Als Gast fortfahren (5 Downloads/Tag)', + style: TextStyle(fontSize: 16)), + onPressed: _alsGastFortfahren, + ), + ), + if (_gastFehler != null) ...[ + const SizedBox(height: 8), + Text(_gastFehler!, + style: const TextStyle(color: MeloTheme.red, fontSize: 13)), + ], + ], + ] else ...[ + if (!_serverUser && auth.istAngemeldet) ...[ Row( children: [ const Icon(Icons.verified_user, size: 18, color: MeloTheme.text3), @@ -787,6 +821,23 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { ), const SizedBox(height: 8), ], + if (istGastModus) ...[ + Row( + children: [ + const Icon(Icons.person_outline, size: 18, color: MeloTheme.text3), + const SizedBox(width: 8), + 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), + ), + ), + ], + ), + const SizedBox(height: 8), + ], TextField( controller: _url, enabled: !dienst.laeuft, @@ -798,14 +849,15 @@ class _YouTubeBereichState extends State<_YouTubeBereich> { ), const SizedBox(height: 12), _KategorieFeld(controller: _kategorie, aktiv: !dienst.laeuft), - SwitchListTile( - dense: true, - contentPadding: EdgeInsets.zero, - title: const Text('YouTube-Cookies des Servers verwenden'), - subtitle: const Text('Nötig für altersbeschränkte Videos'), - value: _cookies, - onChanged: dienst.laeuft ? null : _setzeCookies, - ), + if (!istGastModus) + SwitchListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: const Text('YouTube-Cookies des Servers verwenden'), + subtitle: const Text('Nötig für altersbeschränkte Videos'), + value: _cookies, + onChanged: dienst.laeuft ? null : _setzeCookies, + ), if (_orte.length > 1) ...[ const SizedBox(height: 4), DropdownButtonFormField( diff --git a/test/downloads/online_screen_test.dart b/test/downloads/online_screen_test.dart index 1d21203..d7219ad 100644 --- a/test/downloads/online_screen_test.dart +++ b/test/downloads/online_screen_test.dart @@ -11,6 +11,7 @@ import 'package:melo/library/category_service.dart'; import 'package:melo/library/database.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; +import 'package:melo/services/gast_zugang.dart'; import 'package:melo/services/navidrome_service.dart'; import 'package:melo/services/yt_download_service.dart'; import 'package:provider/provider.dart'; @@ -29,7 +30,9 @@ class _MemorySpeicher implements TokenSpeicher { Widget _wrap(BakaAuth auth, {required MeloDb db, required CategoryService categories, - NavidromeService? navidrome}) { + NavidromeService? navidrome, + GastZugang? gast}) { + final gastZugang = gast ?? GastZugang(); return MultiProvider( providers: [ Provider.value(value: db), @@ -38,9 +41,11 @@ Widget _wrap(BakaAuth auth, // Aufräum-Timer von drift im Test-Rahmen hängen. ChangeNotifierProvider.value(value: categories), ChangeNotifierProvider.value(value: auth), + ChangeNotifierProvider.value(value: gastZugang), ChangeNotifierProvider( create: (_) => YtDownloadService( auth: auth, + gast: gastZugang, client: MockClient((_) async => http.Response('', 500)), ), ), @@ -270,4 +275,63 @@ void main() { expect(gesendeterBenutzer, 'Tinker'); expect(auth.istAngemeldet, isTrue); }); + + testWidgets('Gast sieht zwei Buttons: Anmelden und Als Gast fortfahren', + (tester) async { + final auth = BakaAuth( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher()); + + await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); + await tester.pumpAndSettle(); + + expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); + expect( + find.text('Als Gast fortfahren (5 Downloads/Tag)'), findsOneWidget); + expect(find.text('YouTube-Adresse'), findsNothing); + }); + + testWidgets( + 'Als Gast fortfahren holt einen Token und schaltet das Adressfeld frei', + (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); + expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget); + expect(find.text('YouTube-Cookies des Servers verwenden'), findsNothing); + }); + + testWidgets('Fehler beim Gast-Token wird angezeigt', (tester) async { + final auth = BakaAuth( + client: MockClient((_) async => http.Response('', 500)), + speicher: _MemorySpeicher()); + final gast = GastZugang( + client: MockClient((_) async => http.Response('', 500)), + 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('Gast-Zugang fehlgeschlagen (500)'), findsOneWidget); + expect(find.text('YouTube-Adresse'), findsNothing); + }); } diff --git a/test/hauptmenue_test.dart b/test/hauptmenue_test.dart index 2f08869..56e9c3d 100644 --- a/test/hauptmenue_test.dart +++ b/test/hauptmenue_test.dart @@ -9,6 +9,7 @@ import 'package:melo/main.dart'; import 'package:melo/player/audio_handler.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; +import 'package:melo/services/gast_zugang.dart'; import 'package:melo/services/melo_cloud_service.dart'; import 'package:melo/services/navidrome_service.dart'; import 'package:melo/services/offline_mode.dart'; @@ -51,6 +52,7 @@ void main() { late YtSearchService ytSearch; late DownloadService downloads; late SyncService sync; + late GastZugang gast; MeloAudioHandler? handler; setUp(() { @@ -62,8 +64,9 @@ void main() { auth = BakaAuth(speicher: _MemorySpeicher()); einstellungen = AppSettings(); offline = OfflineMode(); - ytDownload = YtDownloadService(auth: auth); - ytSearch = YtSearchService(auth: auth); + gast = GastZugang(); + ytDownload = YtDownloadService(auth: auth, gast: gast); + ytSearch = YtSearchService(auth: auth, gast: gast); downloads = DownloadService(db: db, navidrome: NavidromeService()); sync = SyncService(db: db, cloud: MeloCloudService(auth: auth)); }); @@ -95,6 +98,7 @@ void main() { ChangeNotifierProvider.value(value: einstellungen), ChangeNotifierProvider.value(value: offline), ChangeNotifierProvider.value(value: auth), + ChangeNotifierProvider.value(value: gast), ChangeNotifierProvider.value(value: ytDownload), ChangeNotifierProvider.value(value: ytSearch), ChangeNotifierProvider.value(value: downloads), diff --git a/test/home_shell_test.dart b/test/home_shell_test.dart index c5b87e7..6ad745b 100644 --- a/test/home_shell_test.dart +++ b/test/home_shell_test.dart @@ -9,6 +9,7 @@ import 'package:melo/main.dart'; import 'package:melo/player/audio_handler.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; +import 'package:melo/services/gast_zugang.dart'; import 'package:melo/services/melo_cloud_service.dart'; import 'package:melo/services/navidrome_service.dart'; import 'package:melo/services/offline_mode.dart'; @@ -50,6 +51,7 @@ void main() { ChangeNotifierProvider(create: (_) => AppSettings()), ChangeNotifierProvider(create: (_) => OfflineMode()), ChangeNotifierProvider(create: (_) => auth), + ChangeNotifierProvider(create: (_) => GastZugang()), ChangeNotifierProvider( create: (_) => YtDownloadService(auth: auth)), ChangeNotifierProvider(