import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../library/database.dart'; import '../library/library_service.dart'; import '../services/baka_auth.dart'; import '../services/download_service.dart'; import '../services/media_store.dart'; import '../services/yt_download_service.dart'; import '../services/yt_search_service.dart'; import '../shared/theme.dart'; import 'download_einordnung.dart'; /// Extra-Tab (zum Testen): YouTube direkt in der App durchsuchen, statt nur /// eine Adresse einzufügen. Ergebnisse zeigen Vorschaubild, Titel und Dauer; /// der Download-Knopf nutzt denselben Baka-Proxy-Weg wie der Download-Tab. class YoutubeSearchScreen extends StatefulWidget { const YoutubeSearchScreen({super.key}); @override State createState() => _YoutubeSearchScreenState(); } class _YoutubeSearchScreenState extends State { final _query = TextEditingController(); /// Server-User (siehe _YouTubeBereich in downloads_screen.dart) brauchen /// hier keinen Anmelde-Hinweis — die Anmeldung passiert im Hintergrund. bool _serverUser = false; /// Die automatische Anmeldung ist fehlgeschlagen — dann bleibt der /// Hinweis auf die manuelle Anmeldung (siehe Download-Tab) sichtbar. bool _autoLoginFehlgeschlagen = false; @override void initState() { super.initState(); _pruefeServerUser(); } Future _pruefeServerUser() async { final nav = context.read().navidrome; final auth = context.read(); final ergebnis = await pruefeServerUser(nav, auth); if (!mounted) return; setState(() { _serverUser = ergebnis.serverUser; _autoLoginFehlgeschlagen = ergebnis.autoLoginFehlgeschlagen; }); } @override void dispose() { _query.dispose(); super.dispose(); } void _suchen() { final q = _query.text.trim(); if (q.isEmpty) return; FocusScope.of(context).unfocus(); context.read().suchen(q); } @override Widget build(BuildContext context) { final auth = context.watch(); final suche = context.watch(); final zugriffOk = auth.istAngemeldet || (_serverUser && !_autoLoginFehlgeschlagen); return SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Row( children: [ Expanded( child: TextField( controller: _query, textInputAction: TextInputAction.search, onSubmitted: (_) => _suchen(), decoration: const InputDecoration( hintText: 'YouTube durchsuchen …', prefixIcon: Icon(Icons.search), ), ), ), const SizedBox(width: 8), FilledButton( onPressed: _suchen, child: const Text('Suchen'), ), ], ), ), if (!zugriffOk) const Padding( padding: 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).', style: TextStyle(color: MeloTheme.text2, fontSize: 13), ), ), if (suche.laeuft) const LinearProgressIndicator(), if (suche.fehler != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text(suche.fehler!, style: const TextStyle(color: Colors.redAccent)), ), Expanded( child: suche.treffer.isEmpty ? const _Empty() : ListView.builder( padding: const EdgeInsets.only(bottom: 16), itemCount: suche.treffer.length, itemBuilder: (context, i) => _TrefferZeile(treffer: suche.treffer[i]), ), ), ], ), ); } } class _Empty extends StatelessWidget { const _Empty(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.smart_display_outlined, size: 64, color: Colors.white24), const SizedBox(height: 12), const Text('Nach Musikvideos suchen', style: TextStyle(color: MeloTheme.text2)), ], ), ); } } class _TrefferZeile extends StatefulWidget { const _TrefferZeile({required this.treffer}); final YtSuchTreffer treffer; @override State<_TrefferZeile> createState() => _TrefferZeileState(); } class _TrefferZeileState extends State<_TrefferZeile> { bool _laedt = false; Future _herunterladen() async { final dienst = context.read(); final lib = context.read(); final db = context.read(); final messenger = ScaffoldMessenger.of(context); setState(() => _laedt = true); // Erst in einen App-eigenen Zwischenordner — siehe Download-Tab // (_YouTubeBereich in downloads_screen.dart) für denselben Ablauf. final zwischen = Directory.systemTemp.createTempSync('melo_yt').path; final ergebnis = await dienst.herunterladen(widget.treffer.url, zielOrdner: zwischen); if (!mounted) return; if (ergebnis == null) { setState(() => _laedt = false); messenger.showSnackBar( SnackBar(content: Text(dienst.fehler ?? 'Fehlgeschlagen'))); return; } final pfad = await const MediaStore().veroeffentliche( quellPfad: ergebnis.dateiPfad, titel: ergebnis.titel, kuenstler: ergebnis.kuenstler, ); if (!mounted) return; setState(() => _laedt = false); if (pfad == null) { messenger.showSnackBar(const SnackBar( content: Text('Heruntergeladen, aber nicht im Musikordner ablegbar'))); return; } // Der MediaStore kennt die Datei jetzt; ein Scan holt sie in die // Bibliothek. Ohne gewählte Kategorie, wie bei einem schnellen Fund. await lib.rescan(); await ordneDownloadEin(db, pfad, ''); if (mounted) { messenger.showSnackBar( SnackBar(content: Text('✅ ${ergebnis.titel} ist in deiner Musik'))); } } @override Widget build(BuildContext context) { final t = widget.treffer; return ListTile( leading: SizedBox( width: 96, height: 54, child: t.thumbnailUrl == null ? const _ThumbnailPlatzhalter() : ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( t.thumbnailUrl!, fit: BoxFit.cover, errorBuilder: (_, _, _) => const _ThumbnailPlatzhalter(), ), ), ), title: Text(t.title, maxLines: 2, overflow: TextOverflow.ellipsis), subtitle: Text(_dauerLabel(t.duration)), trailing: _laedt ? const SizedBox( width: MeloTheme.minTouchTarget, height: MeloTheme.minTouchTarget, child: Center( child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ), ) : IconButton( tooltip: 'Herunterladen', icon: const Icon(Icons.download_for_offline_outlined), onPressed: _herunterladen, ), ); } } class _ThumbnailPlatzhalter extends StatelessWidget { const _ThumbnailPlatzhalter(); @override Widget build(BuildContext context) { return const ColoredBox( color: MeloTheme.surfaceHigh, child: Center( child: Icon(Icons.smart_display_outlined, color: Colors.white24), ), ); } } /// "3:07" — Minuten:Sekunden, wie überall sonst in der App. String _dauerLabel(Duration d) { final minuten = d.inMinutes; final sekunden = d.inSeconds % 60; return '$minuten:${sekunden.toString().padLeft(2, '0')}'; }