YouTube-Gast-Zugang (App-Seite): 5 Downloads/Tag ohne Konto #4

Closed
dustin wants to merge 12 commits from feature/youtube-gast-zugang into fix/p0-vollwertigkeit
4 changed files with 136 additions and 14 deletions
Showing only changes of commit cfebcc03b5 - Show all commits
+55 -3
View File
@@ -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<void> _alsGastFortfahren() async {
final gast = context.read<GastZugang>();
setState(() => _gastFehler = null);
final fehler = await gast.holeToken();
if (!mounted) return;
if (fehler != null) setState(() => _gastFehler = fehler);
}
Future<void> _herunterladen() async {
final dienst = context.read<YtDownloadService>();
final lib = context.read<LibraryService>();
@@ -736,13 +748,17 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
@override
Widget build(BuildContext context) {
final auth = context.watch<BakaAuth>();
final gast = context.watch<GastZugang>();
final dienst = context.watch<YtDownloadService>();
// 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,6 +849,7 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
),
const SizedBox(height: 12),
_KategorieFeld(controller: _kategorie, aktiv: !dienst.laeuft),
if (!istGastModus)
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
+65 -1
View File
@@ -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<MeloDb>.value(value: db),
@@ -38,9 +41,11 @@ Widget _wrap(BakaAuth auth,
// Aufräum-Timer von drift im Test-Rahmen hängen.
ChangeNotifierProvider<CategoryService>.value(value: categories),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<GastZugang>.value(value: gastZugang),
ChangeNotifierProvider<YtDownloadService>(
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);
});
}
+6 -2
View File
@@ -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<AppSettings>.value(value: einstellungen),
ChangeNotifierProvider<OfflineMode>.value(value: offline),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<GastZugang>.value(value: gast),
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
ChangeNotifierProvider<YtSearchService>.value(value: ytSearch),
ChangeNotifierProvider<DownloadService>.value(value: downloads),
+2
View File
@@ -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<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(