import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:just_audio/just_audio.dart'; import '../services/download_service.dart'; import '../services/cloud_service.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; import '../utils/farb_theme.dart'; import '../services/melo_logger.dart'; import '../config/app_config.dart'; import '../widgets/melo_loader.dart'; class DownloadScreen extends StatefulWidget { final DownloadService downloader; final VoidCallback onSongsChanged; const DownloadScreen({ super.key, required this.downloader, required this.onSongsChanged, }); @override State createState() => _DownloadScreenState(); } class _DownloadScreenState extends State with WidgetsBindingObserver { final _urlController = TextEditingController(); final _cloud = CloudService(); final _previewPlayer = AudioPlayer(); bool _ladt = false; List> _globalSongs = []; String? _previewSid; String? _fehler; String? _erfolg; String _speicherOrt = 'App-intern (Music/)'; @override void dispose() { WidgetsBinding.instance.removeObserver(this); _previewPlayer.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { _stopPreview(); } } Future _ladeGlobalListe() async { final songs = await _cloud.globalList(); if (mounted) setState(() => _globalSongs = songs.cast>()); } Future _addFromRegistry(String sid, String title) async { setState(() => _ladt = true); try { final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) await dir.create(recursive: true); final dest = '${dir.path}/cloud_$sid.mp3'; final ok = await _cloud.download(sid, dest); if (ok && mounted) { final song = Song( titel: title.isNotEmpty ? title : 'Cloud-Song $sid', kuenstler: 'Melo Registry', dauerSekunden: 0, dateiPfad: dest, downloadQuelle: 'cloud', istHeruntergeladen: true, ); await DbHelper().songEinfuegen(song); setState(() => _erfolg = 'Song "$title" hinzugefΓΌgt!'); widget.onSongsChanged(); await _ladeGlobalListe(); } } catch (e) { setState(() => _fehler = 'Fehler beim HinzufΓΌgen'); MeloLogger().fehler('add_from_registry', e); } if (mounted) setState(() => _ladt = false); } Future _startPreview(String sid) async { if (_previewSid == sid && _previewPlayer.playing) { await _stopPreview(); return; } _previewSid = sid; try { final url = '${AppConfig.cloudUrl}/api/cloud/stream/$sid'; await _previewPlayer.setUrl(url); await _previewPlayer.seek(const Duration(seconds: 11)); await _previewPlayer.play(); Future.delayed(const Duration(seconds: 10), () { if (_previewSid == sid) _stopPreview(); }); } catch (e) { MeloLogger().fehler('preview', e); } } Future _stopPreview() async { _previewSid = null; await _previewPlayer.stop(); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _ladeSpeicherPfad(); _ladeGlobalListe(); } Future _ladeSpeicherPfad() async { final prefs = await SharedPreferences.getInstance(); final inDownloads = prefs.getBool('download_in_downloads') ?? false; if (inDownloads) { final dir = await getDownloadsDirectory(); if (dir != null) { final pfad = '${dir.path}/Melo'; widget.downloader.setzeSpeicherPfad(pfad); setState(() { _speicherOrt = '⬇ Downloads/Melo'; }); } } } Future _ordnerDialog() async { final auswahl = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Speicherort', style: TextStyle(color: Colors.white, fontSize: 16)), content: Column( mainAxisSize: MainAxisSize.min, children: [ _optionTile(ctx, 'πŸ“ App-intern (Music/)', 'intern', icon: Icons.phone_android), if (!Platform.isIOS) ...[ const Divider(color: MeloTheme.dunkel2), _optionTile(ctx, '⬇ Downloads/Melo', 'downloads', icon: Icons.download), const Divider(color: MeloTheme.dunkel2), _optionTile(ctx, 'πŸ’Ύ SD-Karte / Extern', 'extern', icon: Icons.sd_storage), ], ], ), ), ); if (auswahl == null) return; final prefs = await SharedPreferences.getInstance(); if (auswahl == 'downloads') { final dir = await getDownloadsDirectory(); if (dir != null) { final pfad = '${dir.path}/Melo'; await prefs.setBool('download_in_downloads', true); widget.downloader.setzeSpeicherPfad(pfad); setState(() { _speicherOrt = '⬇ Downloads/Melo'; }); } } else if (auswahl == 'extern') { final dirs = await getExternalStorageDirectories(); if (dirs != null && dirs.isNotEmpty) { final pfad = '${dirs.first.path}/Melo'; await prefs.setBool('download_in_downloads', false); widget.downloader.setzeSpeicherPfad(pfad); setState(() { _speicherOrt = 'πŸ’Ύ ${dirs.first.path.split('/').last}/Melo'; }); } else { if (mounted) setState(() => _fehler = 'Kein externer Speicher gefunden'); } } else { await prefs.setBool('download_in_downloads', false); widget.downloader.setzeSpeicherPfad(''); setState(() { _speicherOrt = 'πŸ“ App-intern (Music/)'; }); } } Widget _optionTile(BuildContext ctx, String label, String wert, {required IconData icon}) { return ListTile( leading: Icon(icon, color: MeloTheme.rot, size: 20), title: Text(label, style: const TextStyle(color: Colors.white, fontSize: 13)), onTap: () => Navigator.pop(ctx, wert), ); } void _starteDownload() async { final input = _urlController.text.trim(); if (input.isEmpty) { setState(() => _fehler = 'Bitte eine YouTube-URL einfΓΌgen'); return; } setState(() { _ladt = true; _fehler = null; _erfolg = null; }); MeloLogger().aktion('download_start', {'url': input.substring(0, 40)}); final anzahl = await widget.downloader.downloadBatch(input); if (mounted) { setState(() { _ladt = false; if (anzahl > 0) { _erfolg = 'βœ… $anzahl Song${anzahl > 1 ? 's' : ''} gespeichert'; } else { _fehler = widget.downloader.fehler ?? 'Download fehlgeschlagen'; } }); widget.onSongsChanged(); } } void _abbrechen() { widget.downloader.abbrechen(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: MeloTheme.schwarz, appBar: AppBar( backgroundColor: MeloTheme.dunkel1, title: const Row(children: [ Icon(Icons.download, color: MeloTheme.rot, size: 20), SizedBox(width: 8), Text('Lied +', style: TextStyle(color: Colors.white, fontSize: 18)), ]), actions: [ if (_erfolg != null || _fehler != null) IconButton( icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), onPressed: () => setState(() { _fehler = null; _erfolg = null; _urlController.clear(); }), ), ], ), body: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ // ─── Globale Registry (Lied +) ─── if (_globalSongs.isNotEmpty) ...[ Row(children: [ const Icon(Icons.public, color: MeloTheme.rot, size: 16), const SizedBox(width: 6), Text('Globale Songs (${_globalSongs.length})', style: const TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w600)), const Spacer(), GestureDetector( onTap: _ladeGlobalListe, child: const Icon(Icons.refresh, color: Colors.grey, size: 16), ), ]), const SizedBox(height: 8), SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _globalSongs.length, itemBuilder: (_, i) { final s = _globalSongs[i]; final sid = s['id']?.toString() ?? ''; final title = s['title']?.toString() ?? '?'; final isPreviewing = _previewSid == sid; return Container( width: 140, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: isPreviewing ? const Color(0xFF2A0000) : MeloTheme.dunkel1, borderRadius: BorderRadius.circular(10), border: Border.all(color: isPreviewing ? MeloTheme.rot : MeloTheme.dunkel2), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title, style: TextStyle(fontSize: 11, color: Colors.white, fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: () => _startPreview(sid), child: Icon(isPreviewing ? Icons.stop : Icons.play_arrow, color: isPreviewing ? Colors.white : MeloTheme.rot, size: 20), ), const SizedBox(width: 10), GestureDetector( onTap: () => _addFromRegistry(sid, title), child: const Icon(Icons.add_circle_outline, color: Colors.grey, size: 18), ), ], ), ], ), ); }, ), ), const Divider(color: MeloTheme.dunkel2), ], // ─── Zielordner ─── GestureDetector( onTap: _ladt ? null : _ordnerDialog, child: Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row(children: [ const Icon(Icons.folder, color: MeloTheme.rot, size: 18), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Speicherort', style: TextStyle(color: Colors.grey, fontSize: 11)), Text(_speicherOrt, style: const TextStyle(color: Colors.white, fontSize: 13)), ], ), ), const Icon(Icons.chevron_right, color: Colors.grey, size: 18), ]), ), ), const SizedBox(height: 12), // ─── Eingabefeld ─── TextField( controller: _urlController, enabled: !_ladt, maxLines: 3, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: 'YouTube-URL hier einfΓΌgen...\n\nMehrere URLs: eine pro Zeile\nPlaylists werden erkannt 🎯', hintStyle: const TextStyle(color: Colors.grey, fontSize: 13), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: MeloTheme.dunkel1, contentPadding: const EdgeInsets.all(16), ), ), const SizedBox(height: 12), // ─── Animierte Ladeanzeige (wΓ€hrend Download) ─── if (_ladt) ...[ MeloLoader( titel: widget.downloader.aktuellerTitel ?? 'Lade herunter...', ), const SizedBox(height: 16), ], // ─── Download-Button ─── SizedBox( width: double.infinity, height: 48, child: ElevatedButton.icon( onPressed: _ladt ? null : _starteDownload, icon: _ladt ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.download, size: 20), label: Text(_ladt ? 'LΓ€dt...' : '⬇ Download'), style: ElevatedButton.styleFrom( backgroundColor: MeloTheme.rot, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), if (_ladt) ...[ const SizedBox(height: 8), SizedBox( width: double.infinity, height: 40, child: ElevatedButton.icon( onPressed: _abbrechen, icon: const Icon(Icons.cancel, size: 18), label: const Text('Abbrechen'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], const SizedBox(height: 16), // ─── Fortschritt ─── if (_ladt) ListenableBuilder( listenable: widget.downloader, builder: (context, _) { final fortschritt = widget.downloader.fortschritt; if (fortschritt <= 0) return const SizedBox.shrink(); return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), ), child: Column(children: [ LinearProgressIndicator( value: fortschritt, color: MeloTheme.rot, backgroundColor: MeloTheme.dunkel2), const SizedBox(height: 4), Text('${(fortschritt * 100).toStringAsFixed(0)}%', style: const TextStyle(color: Colors.grey, fontSize: 11)), ]), ); }, ), // ─── Erfolg ─── if (_erfolg != null) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.green.withValues(alpha: 0.3)), ), child: Row(children: [ const Icon(Icons.check_circle, color: Colors.green, size: 24), const SizedBox(width: 12), Expanded(child: Text(_erfolg!, style: const TextStyle(color: Colors.green, fontSize: 13))), ]), ), // ─── Fehler ─── if (_fehler != null) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.withValues(alpha: 0.3)), ), child: Row(children: [ const Icon(Icons.error_outline, color: Colors.red, size: 24), const SizedBox(width: 12), Expanded(child: Text(_fehler!, style: const TextStyle(color: Colors.red, fontSize: 13))), ]), ), const Spacer(), // ─── Tipps ─── Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('πŸ’‘ Tipps', style: TextStyle(color: Colors.grey, fontSize: 12, fontWeight: FontWeight.w600)), const SizedBox(height: 6), _tipp('Einzel-URL: youtube.com/watch?v=...'), _tipp('Playlist: youtube.com/playlist?list=...'), _tipp('Mehrere: eine URL pro Zeile'), _tipp('Cooldown: 5s zwischen Downloads ⏱'), ], ), ), const SizedBox(height: 20), ], ), ), ); } Widget _tipp(String text) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: Row(children: [ const Text('β€’ ', style: TextStyle(color: MeloTheme.rot, fontSize: 12)), Expanded(child: Text(text, style: const TextStyle(color: Colors.grey, fontSize: 11))), ]), ); } }