import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:just_audio/just_audio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/farb_theme.dart'; import '../services/cloud_service.dart'; import '../services/melo_logger.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; import '../services/id3_reader.dart'; class CloudScreen extends StatefulWidget { final CloudService cloud; final VoidCallback onSongsChanged; const CloudScreen({super.key, required this.cloud, required this.onSongsChanged}); @override State createState() => _CloudScreenState(); } class _CloudScreenState extends State { int _serverCount = 0; bool _ladt = false; String? _status; bool _autoSync = true; int _syncIntervall = 6; String _aktuellerDownload = ''; int _downloadFortschritt = 0; int _downloadGesamt = 0; @override void initState() { super.initState(); _ladeStatus(); _ladeSettings(); } Future _ladeStatus() async { final p = await SharedPreferences.getInstance(); final nutzer = p.getString('melo_nutzer') ?? ''; await widget.cloud.restoreLogin(); if (nutzer.isEmpty || !widget.cloud.istAngemeldet) { if (mounted) setState(() { _serverCount = 0; _status = 'Nicht angemeldet'; }); return; } final st = await widget.cloud.status(); if (!mounted) return; setState(() { _serverCount = st?['total'] ?? 0; _status = st != null ? 'Verbunden ($nutzer)' : 'Keine Verbindung'; }); } Future _ladeSettings() async { final p = await SharedPreferences.getInstance(); if (mounted) setState(() { _autoSync = p.getBool('cloud_auto') ?? true; _syncIntervall = p.getInt('cloud_interval') ?? 6; }); } Future _upload() async { setState(() => _ladt = true); final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) { setState(() { _ladt = false; _status = 'Keine lokalen Songs'; }); return; } final files = dir.listSync().whereType().where((f) => f.path.endsWith('.mp3') || f.path.endsWith('.m4a')); int count = 0; for (final f in files) { final sid = await widget.cloud.upload(f.path, f.path.split('/').last); if (sid != null) count++; } await _ladeStatus(); if (mounted) { setState(() { _ladt = false; _status = '$count Songs hochgeladen'; }); MeloLogger().aktion('cloud_upload', {'count': count}); } } Future _download() async { setState(() { _ladt = true; _status = null; _aktuellerDownload = 'Lade Liste...'; _downloadFortschritt = 0; _downloadGesamt = 0; }); // Auf Android: Music/Melo (getExternalStorageDirectory) // Auf iOS: Documents/music (Sandbox, sichtbar in Dateien-App) String basePath; if (Platform.isIOS) { basePath = '${(await getApplicationDocumentsDirectory()).path}/music'; } else { // Wenn voller Speicherzugriff: Downloads/Melo (sichtbar) final p = await SharedPreferences.getInstance(); if (p.getBool('manage_storage') == true) { final d = await getDownloadsDirectory(); basePath = d != null ? '${d.path}/Melo' : '${(await getApplicationDocumentsDirectory()).path}/music'; } else { final ext = await getExternalStorageDirectory(); basePath = ext != null ? '${ext.path}/Melo' : '${(await getApplicationDocumentsDirectory()).path}/music'; } } final dir = Directory(basePath); if (!await dir.exists()) await dir.create(recursive: true); final localFiles = dir.listSync().whereType() .map((f) => f.path.split('/').last).toSet(); final serverSongs = await widget.cloud.listSongs(); // Filter: nur neue Songs final neue = serverSongs.where((s) { final title = s['title'].toString(); return !localFiles.contains(title); }).toList(); if (neue.isEmpty) { if (mounted) setState(() { _ladt = false; _status = 'Alle Songs bereits lokal'; }); return; } setState(() => _downloadGesamt = neue.length); final db = DbHelper(); for (int i = 0; i < neue.length; i++) { final song = neue[i]; final title = song['title'].toString(); final sid = song['id'].toString(); // Dateinamen säubern (p.basename verhindert Pfad-Traversal via Titel) final safeTitle = p.basename(title).replaceAll(RegExp(r'[^\w\s\.-]'), '_').trim(); final dateiName = safeTitle.endsWith('.mp3') || safeTitle.endsWith('.m4a') ? safeTitle : '$safeTitle.mp3'; final dest = '${dir.path}/$dateiName'; if (mounted) setState(() { _aktuellerDownload = title; _downloadFortschritt = i + 1; }); final ok = await widget.cloud.download(sid, dest); if (ok) { // Dauer auslesen int dauer = 0; try { final ap = AudioPlayer(); await ap.setFilePath(dest).timeout(const Duration(milliseconds: 2000)); dauer = ap.duration?.inSeconds ?? 0; await ap.dispose(); } catch (_) {} // ID3-Metadaten & Cover aus der heruntergeladenen Datei extrahieren final id3 = Id3Reader.lesen(dest); String? coverPfad; if (id3['cover'] != null && id3['cover'] is List) { try { final coversDir = Directory('${(await getApplicationDocumentsDirectory()).path}/covers'); if (!await coversDir.exists()) await coversDir.create(recursive: true); final coverFile = File('${coversDir.path}/cloud_$sid.jpg'); await coverFile.writeAsBytes(id3['cover'] as List); coverPfad = coverFile.path; } catch (_) {} } try { await db.songEinfuegen(Song( titel: (id3['titel'] as String).isNotEmpty ? id3['titel'] : title.replaceAll('.mp3', '').replaceAll('.m4a', ''), kuenstler: (id3['kuenstler'] as String).isNotEmpty ? id3['kuenstler'] : 'Melo Cloud', album: id3['album'] ?? '', dauerSekunden: dauer, dateiPfad: dest, coverPfad: coverPfad, downloadQuelle: 'cloud', istHeruntergeladen: true, )); } catch (_) {} } } await _ladeStatus(); widget.onSongsChanged(); // Musik-Tab aktualisieren if (mounted) { setState(() { _ladt = false; _aktuellerDownload = ''; _status = '${_downloadGesamt} Songs heruntergeladen'; }); MeloLogger().aktion('cloud_download', {'count': _downloadGesamt}); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: MeloTheme.schwarz, appBar: AppBar( backgroundColor: MeloTheme.dunkel1, title: const Row(children: [ Text('☁️', style: TextStyle(fontSize: 20)), SizedBox(width: 8), Text('Cloud Sync', style: TextStyle(color: Colors.white, fontSize: 18)), ]), actions: [ IconButton( icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), onPressed: _ladeStatus, ), ], ), body: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ // Status-Karte Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( children: [ const Icon(Icons.storage, color: MeloTheme.rot, size: 28), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$_serverCount Songs auf Server', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), Text(_status ?? 'Lädt...', style: const TextStyle(color: Colors.grey, fontSize: 12)), ], ), ], ), ), const SizedBox(height: 16), // Upload / Download Row( children: [ Expanded(child: _btn(Icons.upload, 'Upload', _upload)), const SizedBox(width: 12), Expanded(child: _btn(Icons.download, 'Download', _download)), ], ), // Download-Fortschritt if (_ladt && _aktuellerDownload.isNotEmpty) ...[ const SizedBox(height: 12), LinearProgressIndicator( value: _downloadGesamt > 0 ? _downloadFortschritt / _downloadGesamt : null, backgroundColor: MeloTheme.dunkel2, valueColor: const AlwaysStoppedAnimation(MeloTheme.rot), ), const SizedBox(height: 6), Text( '$_downloadFortschritt/$_downloadGesamt: $_aktuellerDownload', style: const TextStyle(color: Colors.grey, fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], if (_status != null && !_ladt) Padding( padding: const EdgeInsets.only(top: 8), child: Text(_status!, style: const TextStyle(color: Colors.grey, fontSize: 13)), ), const SizedBox(height: 12), // Sync-Einstellungen const Divider(color: MeloTheme.dunkel2), const Align( alignment: Alignment.centerLeft, child: Text('⚙ Sync-Einstellungen', style: TextStyle(color: Colors.grey, fontSize: 11)), ), SwitchListTile( dense: true, title: const Text('Auto-Sync', style: TextStyle(color: Colors.white, fontSize: 13)), value: _autoSync, activeColor: MeloTheme.rot, onChanged: (v) async { setState(() => _autoSync = v); (await SharedPreferences.getInstance()).setBool('cloud_auto', v); }, ), Row( children: ['Manuell', 'Alle 3h', 'Alle 6h', 'Alle 12h'].asMap().entries.map((e) { final vals = [0, 3, 6, 12]; return Expanded( child: ChoiceChip( label: Text(e.value, style: TextStyle(fontSize: 10, color: _syncIntervall == vals[e.key] ? Colors.white : Colors.grey)), selected: _syncIntervall == vals[e.key], selectedColor: MeloTheme.rot, backgroundColor: MeloTheme.dunkel2, onSelected: (v) async { setState(() => _syncIntervall = vals[e.key]); (await SharedPreferences.getInstance()).setInt('cloud_interval', vals[e.key]); }, ), ); }).toList(), ), ], ), ), ); } Widget _btn(IconData icon, String label, VoidCallback onTap) { return ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: MeloTheme.dunkel1, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide(color: MeloTheme.dunkel2), ), ), onPressed: _ladt ? null : onTap, icon: Icon(icon, size: 18, color: MeloTheme.rot), label: Text(label, style: const TextStyle(color: Colors.white, fontSize: 14)), ); } }