Merge branch 'feature/youtube-gast-zugang'

# Conflicts:
#	CHANGELOG.md
#	test/downloads/online_screen_test.dart
This commit is contained in:
Hermes (Server)
2026-08-30 10:34:58 +02:00
19 changed files with 1138 additions and 36 deletions
+92 -11
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,12 @@ 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;
/// Verhindert Mehrfach-Anfragen bei schnellem Doppel-Antippen.
bool _gastLaeuft = false;
@override
void initState() {
super.initState();
@@ -686,6 +693,21 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
);
}
Future<void> _alsGastFortfahren() async {
if (_gastLaeuft) return;
final gast = context.read<GastZugang>();
setState(() {
_gastFehler = null;
_gastLaeuft = true;
});
final fehler = await gast.holeToken();
if (!mounted) return;
setState(() {
_gastLaeuft = false;
if (fehler != null) _gastFehler = fehler;
});
}
Future<void> _herunterladen() async {
final dienst = context.read<YtDownloadService>();
final lib = context.read<LibraryService>();
@@ -737,13 +759,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 istGastModus = !_serverUser && !auth.istAngemeldet && gast.hatToken;
final zugriffOk = auth.istAngemeldet ||
(_serverUser && !_autoLoginFehlgeschlagen) ||
istGastModus;
return ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
@@ -770,8 +796,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: _gastLaeuft ? null : _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),
@@ -788,6 +832,42 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
),
const SizedBox(height: 8),
],
if (auth.istAngemeldet && auth.verbleibend != null) ...[
Row(
children: [
const Icon(Icons.cloud_outlined, size: 18, color: MeloTheme.text3),
const SizedBox(width: 8),
Expanded(
child: Text(
'Noch ${auth.verbleibend} von 100 heute',
style: const TextStyle(color: MeloTheme.text2, fontSize: 13),
),
),
],
),
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),
),
),
TextButton(
onPressed: gast.verwerfen,
child: const Text('Anmelden'),
),
],
),
const SizedBox(height: 8),
],
TextField(
controller: _url,
enabled: !dienst.laeuft,
@@ -799,14 +879,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<String>(