import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/farb_theme.dart'; import '../services/cloud_service.dart'; import '../services/auth_service.dart'; import '../services/melo_logger.dart'; class CloudScreen extends StatefulWidget { final CloudService cloud; const CloudScreen({super.key, required this.cloud}); @override State createState() => _CloudScreenState(); } class _CloudScreenState extends State { int _serverCount = 0; bool _ladt = false; String? _status; bool _statusOk = false; bool _autoSync = true; int _syncIntervall = 6; Timer? _syncTimer; String _letzterSync = 'Nie'; List _korrupteSongs = []; bool _ladtKorrupt = false; bool _hatGeprueft = false; @override void initState() { super.initState(); _status = 'Verbinde...'; _verbindeCloud(); _ladeStatus(); _ladeSettings(); } @override void dispose() { _syncTimer?.cancel(); super.dispose(); } Future _verbindeCloud() async { final user = AuthService().benutzer; if (user.isNotEmpty) { final ok = await widget.cloud.login(user); if (!ok && mounted) { setState(() => _setzeStatus('Cloud-Login fehlgeschlagen', ok: false)); } } } Future _ladeSettings() async { final p = await SharedPreferences.getInstance(); final letzter = p.getString('cloud_last_sync'); if (mounted) { setState(() { _autoSync = p.getBool('cloud_auto') ?? true; _syncIntervall = p.getInt('cloud_interval') ?? 6; _letzterSync = letzter ?? 'Nie'; }); } _starteAutoSync(); } void _starteAutoSync() { _syncTimer?.cancel(); if (!_autoSync || _syncIntervall == 0) return; _syncTimer = Timer.periodic( Duration(hours: _syncIntervall), (_) => _autoSyncDurchfuehren(), ); } Future _autoSyncDurchfuehren() async { await _download(); final p = await SharedPreferences.getInstance(); final now = DateTime.now(); final zeit = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; await p.setString('cloud_last_sync', zeit); if (mounted) setState(() => _letzterSync = zeit); } Future _ladeStatus() async { final st = await widget.cloud.status(); if (!mounted) return; setState(() { _serverCount = st?['total'] ?? 0; _statusOk = st != null; _status = st != null ? 'Verbunden' : 'Keine Verbindung (Token?)'; }); } Future _ladeKorrupteSongs() async { setState(() { _ladtKorrupt = true; _hatGeprueft = false; }); try { final corrupted = await widget.cloud.getCorrupted(); if (mounted) setState(() { _korrupteSongs = corrupted; _hatGeprueft = true; }); } catch (e) { MeloLogger().fehler('cloud_corrupted_laden', e); if (mounted) setState(() => _hatGeprueft = true); } finally { if (mounted) setState(() => _ladtKorrupt = false); } } Future _upload() async { setState(() => _ladt = true); _setzeStatus('Suche lokale Songs...'); try { final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) { setState(() { _ladt = false; _setzeStatus('Keine lokalen Songs', ok: false); }); return; } final files = dir.listSync().whereType().where((f) => f.path.endsWith('.mp3') || f.path.endsWith('.m4a')); int count = 0; for (final f in files) { _setzeStatus('Upload: ${f.path.split('/').last}...'); final sid = await widget.cloud.upload(f.path, f.path.split('/').last); if (sid != null) count++; } await _ladeStatus(); if (mounted) { setState(() { _ladt = false; }); _setzeStatus('$count Songs hochgeladen', ok: count > 0); MeloLogger().aktion('cloud_upload', {'count': count}); } } catch (e) { MeloLogger().fehler('cloud_upload_path', e); if (mounted) setState(() { _ladt = false; _setzeStatus('Fehler beim Upload', ok: false); }); } } Future _download() async { setState(() { _ladt = true; }); _setzeStatus('Vergleiche mit Server...'); try { final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); 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(); int downloaded = 0; for (final song in serverSongs) { final title = (song['title'] ?? 'unknown').toString(); if (localFiles.contains(title)) continue; _setzeStatus('Download: $title...'); final sid = song['id'].toString(); final dest = '${dir.path}/$title'; if (await widget.cloud.download(sid, dest)) downloaded++; } await _ladeStatus(); if (mounted) { setState(() { _ladt = false; }); _setzeStatus('$downloaded Songs heruntergeladen', ok: true); MeloLogger().aktion('cloud_download', {'count': downloaded}); } } catch (e) { MeloLogger().fehler('cloud_download_path', e); if (mounted) setState(() { _ladt = false; _setzeStatus('Fehler beim Download', ok: false); }); } } void _setzeStatus(String msg, {bool ok = false}) { if (mounted) setState(() { _status = msg; _statusOk = ok; }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: MeloTheme.schwarz, appBar: AppBar( backgroundColor: MeloTheme.dunkel1, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22), onPressed: () => Navigator.pop(context), ), title: const Row( children: [ Text('☁️', style: TextStyle(fontSize: 20)), SizedBox(width: 8), Text('Cloud Sync', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600)), ], ), actions: [ IconButton( icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), onPressed: _ladeStatus, ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ─── Status-Karte ─── _statusKarte(), const SizedBox(height: 20), // ─── Upload / Download Buttons ─── Row( children: [ Expanded(child: _aktionsButton( icon: Icons.upload_rounded, label: 'Upload', beschreibung: 'Lokale Songs → Server', onTap: _upload, )), const SizedBox(width: 12), Expanded(child: _aktionsButton( icon: Icons.download_rounded, label: 'Download', beschreibung: 'Server → Lokal', onTap: _download, )), ], ), // Lade-Indikator if (_ladt) const Padding( padding: EdgeInsets.only(top: 16), child: Center(child: CircularProgressIndicator(color: MeloTheme.rot)), ), // Status-Text if (_status != null && !_ladt) Padding( padding: const EdgeInsets.only(top: 12), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _statusOk ? const Color(0xFF0D3B1E) : const Color(0xFF3B0D0D), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Icon( _statusOk ? Icons.check_circle : Icons.info_outline, size: 16, color: _statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350), ), const SizedBox(width: 8), Expanded( child: Text( _status!, style: TextStyle( color: _statusOk ? const Color(0xFFA5D6A7) : const Color(0xFFEF9A9A), fontSize: 13, ), ), ), ], ), ), ), const SizedBox(height: 24), // ─── Sync-Einstellungen ─── _sektionsHeader('⚙ Sync-Einstellungen'), const SizedBox(height: 8), // Auto-Sync Toggle Container( decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: SwitchListTile( title: const Text('Auto-Sync', style: TextStyle(color: Colors.white, fontSize: 14)), subtitle: Text( _autoSync ? 'Automatisch synchronisieren' : 'Nur manuell', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), value: _autoSync, activeColor: MeloTheme.rot, secondary: const Icon(Icons.sync, color: MeloTheme.rot, size: 22), onChanged: (v) async { setState(() => _autoSync = v); (await SharedPreferences.getInstance()).setBool('cloud_auto', v); _starteAutoSync(); }, ), ), const SizedBox(height: 12), // Sync-Intervall – schöne Segmented Buttons const Text( 'Intervall', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), _intervallAuswahl(), const SizedBox(height: 20), // ─── Korrupte Songs (vom Server) ─── _sektionsHeader('⚠️ Defekte Musik (Server)'), const SizedBox(height: 8), _korrupteSektion(), const SizedBox(height: 20), // Letzter Sync Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( children: [ const Icon(Icons.history, color: MeloTheme.textSekundaer, size: 18), const SizedBox(width: 10), const Text('Letzter Sync: ', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13)), Text( _letzterSync, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500), ), ], ), ), const SizedBox(height: 32), ], ), ), ); } Widget _statusKarte() { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF1A0000), Color(0xFF0D0D0D)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(18), border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.3)), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: MeloTheme.rot.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(14), ), child: const Icon(Icons.storage_rounded, color: MeloTheme.rot, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$_serverCount', style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w700, ), ), const Text( 'Songs auf dem Server', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), ], ), ), // Verbindungsstatus-Indikator Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: _statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350), boxShadow: [ BoxShadow( color: (_statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350)).withValues(alpha: 0.5), blurRadius: 8, ), ], ), ), ], ), ); } Widget _aktionsButton({ required IconData icon, required String label, required String beschreibung, required VoidCallback onTap, }) { return Material( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: _ladt ? null : onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( children: [ Icon(icon, color: MeloTheme.rot, size: 28), const SizedBox(height: 8), Text(label, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text(beschreibung, textAlign: TextAlign.center, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), ], ), ), ), ); } Widget _intervallAuswahl() { final optionen = [ _IntervallOption('Manuell', 0, Icons.block), _IntervallOption('3 Std', 3, Icons.timer), _IntervallOption('6 Std', 6, Icons.timer), _IntervallOption('12 Std', 12, Icons.timer), ]; return Row( children: optionen.map((opt) { final istAktiv = _syncIntervall == opt.wert; return Expanded( child: Padding( padding: EdgeInsets.only( right: opt != optionen.last ? 8 : 0, ), child: GestureDetector( onTap: () async { setState(() => _syncIntervall = opt.wert); (await SharedPreferences.getInstance()).setInt('cloud_interval', opt.wert); _starteAutoSync(); }, child: AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 6), decoration: BoxDecoration( color: istAktiv ? MeloTheme.rot : MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all( color: istAktiv ? MeloTheme.rot : MeloTheme.dunkel2, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( opt.icon, size: 16, color: istAktiv ? Colors.white : MeloTheme.textSekundaer, ), const SizedBox(height: 6), Text( opt.label, textAlign: TextAlign.center, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: istAktiv ? Colors.white : MeloTheme.textSekundaer, ), ), ], ), ), ), ), ); }).toList(), ); } Widget _sektionsHeader(String titel) { return Text( titel, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 1.2, ), ); } Widget _korrupteSektion() { return Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Button zum Abrufen Row( children: [ Expanded( child: GestureDetector( onTap: _ladtKorrupt ? null : _ladeKorrupteSongs, child: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), decoration: BoxDecoration( color: MeloTheme.rot.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_ladtKorrupt) const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( color: MeloTheme.rot, strokeWidth: 2, ), ) else const Icon(Icons.warning_amber_rounded, color: MeloTheme.rot, size: 16), const SizedBox(width: 8), Text( _ladtKorrupt ? 'Prüfe...' : 'Auf defekte Musik prüfen', style: const TextStyle( color: MeloTheme.rot, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ], ), // Ergebnisliste if (_korrupteSongs.isNotEmpty) ...[ const SizedBox(height: 12), const Divider(color: MeloTheme.dunkel2, height: 1), const SizedBox(height: 8), Text( '${_korrupteSongs.length} defekte Songs gefunden:', style: const TextStyle( color: Color(0xFFEF9A9A), fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), ..._korrupteSongs.map((s) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ const Text('⚠️', style: TextStyle(fontSize: 13)), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( s['title']?.toString() ?? 'Unbekannt', style: const TextStyle( color: Colors.white70, fontSize: 12, decoration: TextDecoration.lineThrough, ), ), if (s['reason'] != null) Text( s['reason'].toString(), style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 10, ), ), ], ), ), ], ), )), ] else if (!_ladtKorrupt && _hatGeprueft && _korrupteSongs.isEmpty) const Padding( padding: EdgeInsets.only(top: 8), child: Text( 'Keine defekten Songs auf dem Server', style: TextStyle(color: Color(0xFFA5D6A7), fontSize: 12), ), ), ], ), ); } } class _IntervallOption { final String label; final int wert; final IconData icon; const _IntervallOption(this.label, this.wert, this.icon); }