fix: Gast-Zugang-Sperre, veralteter Token, Doppel-Tap, Hinweistext (Branch-Review)

Behebt die Findings aus dem abschließenden Gesamt-Branch-Review für
feature/youtube-gast-zugang, alle in einem Rutsch:

- KRITISCH: Gast-Modus war eine Einbahnstraße — einmal "Als Gast
  fortfahren" getippt, war _AnmeldeDialog (einziger Baka-Login-Einstieg,
  auch für Melo-Cloud-Sync) dauerhaft unerreichbar. Neu:
  GastZugang.verwerfen() plus "Anmelden"-Knopf neben der Gast-Statuszeile
  in beiden Screens.
- WICHTIG: zugriffOk prüfte den rohen gast.hatToken statt istGastModus —
  ein alter Gast-Token konnte den Auto-Login-Rückfallweg für Server-User
  verdecken. istGastModus wird jetzt vor zugriffOk berechnet und dort
  verwendet.
- WICHTIG: CHANGELOG ergänzt — der Server-Endpunkt POST /api/guest-token
  fehlt noch (separates Vorhaben).
- Doppel-Tap-Schutz für den Gast-Button in beiden Screens.
- Widersprüchlicher Hinweistext im YT-Suche-Tab (verlangte Baka-Login
  direkt über dem Gast-Button) korrigiert.
- Zwei Gast-Tests in youtube_search_screen_test.dart bekommen jetzt
  echtes gast:-Wiring in YtSearchService/YtDownloadService.
- 2 neue Regressionstests fürs Anmelden-Escape-Hatch.

Getestet: volle Suite grün (602 Tests, 1 skipped, 0 failed),
flutter analyze ohne Befund.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX
This commit is contained in:
Hermes (Server)
2026-08-26 22:17:03 +02:00
co-authored by Claude Sonnet 5
parent b32f3198f8
commit d20f01adee
6 changed files with 175 additions and 23 deletions
+19 -5
View File
@@ -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<void> _alsGastFortfahren() async {
if (_gastLaeuft) return;
final gast = context.read<GastZugang>();
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<void> _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),
+36 -12
View File
@@ -38,6 +38,9 @@ class _YoutubeSearchScreenState extends State<YoutubeSearchScreen> {
/// 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<YoutubeSearchScreen> {
}
Future<void> _alsGastFortfahren() async {
if (_gastLaeuft) return;
final gast = context.read<GastZugang>();
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<YoutubeSearchScreen> {
final auth = context.watch<BakaAuth>();
final gast = context.watch<GastZugang>();
final suche = context.watch<YtSearchService>();
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<YoutubeSearchScreen> {
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<YoutubeSearchScreen> {
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<YoutubeSearchScreen> {
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(),
+11
View File
@@ -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<void> verwerfen() async {
_token = null;
_verbleibend = null;
await _speicher.loeschen(_tokenKey);
notifyListeners();
}
}