YouTube-Gast-Zugang (App-Seite): 5 Downloads/Tag ohne Konto #4
@@ -11,6 +11,7 @@ import '../player/audio_handler.dart';
|
|||||||
import '../services/baka_auth.dart';
|
import '../services/baka_auth.dart';
|
||||||
import '../services/media_store.dart';
|
import '../services/media_store.dart';
|
||||||
import '../services/download_service.dart';
|
import '../services/download_service.dart';
|
||||||
|
import '../services/gast_zugang.dart';
|
||||||
import '../services/navidrome_service.dart';
|
import '../services/navidrome_service.dart';
|
||||||
import '../services/server_neuheiten.dart';
|
import '../services/server_neuheiten.dart';
|
||||||
import '../services/yt_download_service.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.
|
/// sichtbar, sonst käme so ein User nie mehr an den Dialog heran.
|
||||||
bool _autoLoginFehlgeschlagen = false;
|
bool _autoLoginFehlgeschlagen = false;
|
||||||
|
|
||||||
|
/// Fehlertext, falls das Holen des Gast-Tokens scheitert.
|
||||||
|
String? _gastFehler;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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 {
|
Future<void> _herunterladen() async {
|
||||||
final dienst = context.read<YtDownloadService>();
|
final dienst = context.read<YtDownloadService>();
|
||||||
final lib = context.read<LibraryService>();
|
final lib = context.read<LibraryService>();
|
||||||
@@ -736,13 +748,17 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final auth = context.watch<BakaAuth>();
|
final auth = context.watch<BakaAuth>();
|
||||||
|
final gast = context.watch<GastZugang>();
|
||||||
final dienst = context.watch<YtDownloadService>();
|
final dienst = context.watch<YtDownloadService>();
|
||||||
// Server-User gelten sofort als berechtigt — die Anmeldung passiert für
|
// Server-User gelten sofort als berechtigt — die Anmeldung passiert für
|
||||||
// sie im Hintergrund (_pruefeServerUser). Nur wenn die automatische
|
// sie im Hintergrund (_pruefeServerUser). Nur wenn die automatische
|
||||||
// Anmeldung nachweislich fehlgeschlagen ist, bleibt der manuelle Weg
|
// Anmeldung nachweislich fehlgeschlagen ist, bleibt der manuelle Weg
|
||||||
// als Rückfalloption sichtbar — sonst gäbe es keinen Weg mehr zurück.
|
// als Rückfalloption sichtbar — sonst gäbe es keinen Weg mehr zurück.
|
||||||
final zugriffOk =
|
// Ein Gast-Token zählt ebenfalls als Zugang, nur mit Tages-Limit.
|
||||||
auth.istAngemeldet || (_serverUser && !_autoLoginFehlgeschlagen);
|
final zugriffOk = auth.istAngemeldet ||
|
||||||
|
(_serverUser && !_autoLoginFehlgeschlagen) ||
|
||||||
|
gast.hatToken;
|
||||||
|
final istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||||
@@ -769,8 +785,26 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
onPressed: _anmelden,
|
onPressed: _anmelden,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
|
||||||
if (!_serverUser) ...[
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.verified_user, size: 18, color: MeloTheme.text3),
|
const Icon(Icons.verified_user, size: 18, color: MeloTheme.text3),
|
||||||
@@ -787,6 +821,23 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
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(
|
TextField(
|
||||||
controller: _url,
|
controller: _url,
|
||||||
enabled: !dienst.laeuft,
|
enabled: !dienst.laeuft,
|
||||||
@@ -798,14 +849,15 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_KategorieFeld(controller: _kategorie, aktiv: !dienst.laeuft),
|
_KategorieFeld(controller: _kategorie, aktiv: !dienst.laeuft),
|
||||||
SwitchListTile(
|
if (!istGastModus)
|
||||||
dense: true,
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
dense: true,
|
||||||
title: const Text('YouTube-Cookies des Servers verwenden'),
|
contentPadding: EdgeInsets.zero,
|
||||||
subtitle: const Text('Nötig für altersbeschränkte Videos'),
|
title: const Text('YouTube-Cookies des Servers verwenden'),
|
||||||
value: _cookies,
|
subtitle: const Text('Nötig für altersbeschränkte Videos'),
|
||||||
onChanged: dienst.laeuft ? null : _setzeCookies,
|
value: _cookies,
|
||||||
),
|
onChanged: dienst.laeuft ? null : _setzeCookies,
|
||||||
|
),
|
||||||
if (_orte.length > 1) ...[
|
if (_orte.length > 1) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:melo/library/category_service.dart';
|
|||||||
import 'package:melo/library/database.dart';
|
import 'package:melo/library/database.dart';
|
||||||
import 'package:melo/services/baka_auth.dart';
|
import 'package:melo/services/baka_auth.dart';
|
||||||
import 'package:melo/services/download_service.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/navidrome_service.dart';
|
||||||
import 'package:melo/services/yt_download_service.dart';
|
import 'package:melo/services/yt_download_service.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -29,7 +30,9 @@ class _MemorySpeicher implements TokenSpeicher {
|
|||||||
Widget _wrap(BakaAuth auth,
|
Widget _wrap(BakaAuth auth,
|
||||||
{required MeloDb db,
|
{required MeloDb db,
|
||||||
required CategoryService categories,
|
required CategoryService categories,
|
||||||
NavidromeService? navidrome}) {
|
NavidromeService? navidrome,
|
||||||
|
GastZugang? gast}) {
|
||||||
|
final gastZugang = gast ?? GastZugang();
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<MeloDb>.value(value: db),
|
Provider<MeloDb>.value(value: db),
|
||||||
@@ -38,9 +41,11 @@ Widget _wrap(BakaAuth auth,
|
|||||||
// Aufräum-Timer von drift im Test-Rahmen hängen.
|
// Aufräum-Timer von drift im Test-Rahmen hängen.
|
||||||
ChangeNotifierProvider<CategoryService>.value(value: categories),
|
ChangeNotifierProvider<CategoryService>.value(value: categories),
|
||||||
ChangeNotifierProvider<BakaAuth>.value(value: auth),
|
ChangeNotifierProvider<BakaAuth>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<GastZugang>.value(value: gastZugang),
|
||||||
ChangeNotifierProvider<YtDownloadService>(
|
ChangeNotifierProvider<YtDownloadService>(
|
||||||
create: (_) => YtDownloadService(
|
create: (_) => YtDownloadService(
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
gast: gastZugang,
|
||||||
client: MockClient((_) async => http.Response('', 500)),
|
client: MockClient((_) async => http.Response('', 500)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -270,4 +275,63 @@ void main() {
|
|||||||
expect(gesendeterBenutzer, 'Tinker');
|
expect(gesendeterBenutzer, 'Tinker');
|
||||||
expect(auth.istAngemeldet, isTrue);
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:melo/main.dart';
|
|||||||
import 'package:melo/player/audio_handler.dart';
|
import 'package:melo/player/audio_handler.dart';
|
||||||
import 'package:melo/services/baka_auth.dart';
|
import 'package:melo/services/baka_auth.dart';
|
||||||
import 'package:melo/services/download_service.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/melo_cloud_service.dart';
|
||||||
import 'package:melo/services/navidrome_service.dart';
|
import 'package:melo/services/navidrome_service.dart';
|
||||||
import 'package:melo/services/offline_mode.dart';
|
import 'package:melo/services/offline_mode.dart';
|
||||||
@@ -51,6 +52,7 @@ void main() {
|
|||||||
late YtSearchService ytSearch;
|
late YtSearchService ytSearch;
|
||||||
late DownloadService downloads;
|
late DownloadService downloads;
|
||||||
late SyncService sync;
|
late SyncService sync;
|
||||||
|
late GastZugang gast;
|
||||||
MeloAudioHandler? handler;
|
MeloAudioHandler? handler;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -62,8 +64,9 @@ void main() {
|
|||||||
auth = BakaAuth(speicher: _MemorySpeicher());
|
auth = BakaAuth(speicher: _MemorySpeicher());
|
||||||
einstellungen = AppSettings();
|
einstellungen = AppSettings();
|
||||||
offline = OfflineMode();
|
offline = OfflineMode();
|
||||||
ytDownload = YtDownloadService(auth: auth);
|
gast = GastZugang();
|
||||||
ytSearch = YtSearchService(auth: auth);
|
ytDownload = YtDownloadService(auth: auth, gast: gast);
|
||||||
|
ytSearch = YtSearchService(auth: auth, gast: gast);
|
||||||
downloads = DownloadService(db: db, navidrome: NavidromeService());
|
downloads = DownloadService(db: db, navidrome: NavidromeService());
|
||||||
sync = SyncService(db: db, cloud: MeloCloudService(auth: auth));
|
sync = SyncService(db: db, cloud: MeloCloudService(auth: auth));
|
||||||
});
|
});
|
||||||
@@ -95,6 +98,7 @@ void main() {
|
|||||||
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||||
ChangeNotifierProvider<OfflineMode>.value(value: offline),
|
ChangeNotifierProvider<OfflineMode>.value(value: offline),
|
||||||
ChangeNotifierProvider<BakaAuth>.value(value: auth),
|
ChangeNotifierProvider<BakaAuth>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<GastZugang>.value(value: gast),
|
||||||
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
|
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
|
||||||
ChangeNotifierProvider<YtSearchService>.value(value: ytSearch),
|
ChangeNotifierProvider<YtSearchService>.value(value: ytSearch),
|
||||||
ChangeNotifierProvider<DownloadService>.value(value: downloads),
|
ChangeNotifierProvider<DownloadService>.value(value: downloads),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:melo/main.dart';
|
|||||||
import 'package:melo/player/audio_handler.dart';
|
import 'package:melo/player/audio_handler.dart';
|
||||||
import 'package:melo/services/baka_auth.dart';
|
import 'package:melo/services/baka_auth.dart';
|
||||||
import 'package:melo/services/download_service.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/melo_cloud_service.dart';
|
||||||
import 'package:melo/services/navidrome_service.dart';
|
import 'package:melo/services/navidrome_service.dart';
|
||||||
import 'package:melo/services/offline_mode.dart';
|
import 'package:melo/services/offline_mode.dart';
|
||||||
@@ -50,6 +51,7 @@ void main() {
|
|||||||
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
|
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
|
||||||
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
|
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
|
||||||
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
|
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
|
||||||
|
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
|
||||||
ChangeNotifierProvider<YtDownloadService>(
|
ChangeNotifierProvider<YtDownloadService>(
|
||||||
create: (_) => YtDownloadService(auth: auth)),
|
create: (_) => YtDownloadService(auth: auth)),
|
||||||
ChangeNotifierProvider<YtSearchService>(
|
ChangeNotifierProvider<YtSearchService>(
|
||||||
|
|||||||
Reference in New Issue
Block a user