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 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../utils/farb_theme.dart'; import '../utils/sanitize.dart'; import '../services/cloud_service.dart'; import '../services/auth_service.dart'; import '../database/db_helper.dart'; import '../services/melo_logger.dart'; import '../main.dart'; // notificationsPlugin /// Melo Cloud Sync Screen v3 — vollständiges Sync-System /// Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen class CloudScreen extends StatefulWidget { final CloudService cloud; final VoidCallback? onZurueck; const CloudScreen({super.key, required this.cloud, this.onZurueck}); @override State createState() => _CloudScreenState(); } class _CloudScreenState extends State { // ─── Status ─── int _serverCount = 0; int _favServerCount = 0; int _playlistServerCount = 0; bool _ladt = false; String? _status; bool _statusOk = false; bool _verbunden = false; // ─── Sync-Einstellungen ─── bool _autoSync = true; int _syncIntervall = 6; Timer? _syncTimer; String _letzterSync = 'Nie'; String? _naechsterSync; // ─── Sync-Fortschritt ─── String _syncPhase = ''; double _syncFortschritt = 0; bool _syncLaeuft = false; int _syncedItems = 0; // ─── Korrupt ─── List _korrupteSongs = []; bool _ladtKorrupt = false; bool _hatGeprueft = false; // ─── Server-Playlisten ─── List _serverPlaylists = []; bool _ladtPlaylists = false; // ─── Server-Favoriten ─── List _serverFavorites = []; bool _ladtFavorites = false; // ─── Rename ─── final _renameTitleCtrl = TextEditingController(); final _renameArtistCtrl = TextEditingController(); @override void initState() { super.initState(); _status = 'Verbinde...'; _verbindeCloud(); _ladeStatus(); _ladeSettings(); _ladeServerDaten(); } @override void dispose() { _syncTimer?.cancel(); _renameTitleCtrl.dispose(); _renameArtistCtrl.dispose(); super.dispose(); } // ─── Verbindung ─── Future _verbindeCloud() async { final user = AuthService().benutzer; if (user.isNotEmpty) { final ok = await widget.cloud.login(user); if (mounted) { setState(() { _verbunden = ok; if (!ok) _setzeStatus('Cloud-Login fehlgeschlagen', ok: false); }); } } } Future _ladeSettings() async { final p = await SharedPreferences.getInstance(); final letzter = p.getString('cloud_last_sync'); final letzterTs = p.getString('cloud_last_sync_ts'); if (mounted) { setState(() { _autoSync = p.getBool('cloud_auto') ?? true; _syncIntervall = p.getInt('cloud_interval') ?? 6; _letzterSync = letzter ?? (letzterTs != null ? _formatZeit(DateTime.tryParse(letzterTs)) : 'Nie'); }); } _berechneNaechstenSync(); _starteAutoSync(); } Future _ladeServerDaten() async { if (!_verbunden) return; // Paralleles Laden await Future.wait([ _ladePlaylists(), _ladeFavorites(), ]); } void _berechneNaechstenSync() { if (!_autoSync || _syncIntervall == 0) { _naechsterSync = null; return; } final now = DateTime.now(); final last = _letzterSync != 'Nie' ? DateTime.tryParse(_letzterSync) : now; if (last != null) { final next = last.add(Duration(hours: _syncIntervall)); if (next.isBefore(now)) { _naechsterSync = 'Jetzt fällig'; } else { _naechsterSync = _formatZeit(next); } } } String _formatZeit(DateTime? dt) { if (dt == null) return 'Nie'; return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } void _starteAutoSync() { _syncTimer?.cancel(); if (!_autoSync || _syncIntervall == 0) return; _syncTimer = Timer.periodic( Duration(hours: _syncIntervall), (_) => _autoSyncDurchfuehren(), ); } Future _autoSyncDurchfuehren() async { if (_syncLaeuft) return; await _syncAlles(); final p = await SharedPreferences.getInstance(); final now = DateTime.now(); final zeit = _formatZeit(now); await p.setString('cloud_last_sync', zeit); await p.setString('cloud_last_sync_ts', now.toIso8601String()); if (mounted) { setState(() { _letzterSync = zeit; }); _berechneNaechstenSync(); } } // ─── 🔄 Komplett-Sync ─── Future _syncAlles() async { if (_syncLaeuft) return; setState(() { _syncLaeuft = true; _syncPhase = 'Verbinde...'; _syncFortschritt = 0; _syncedItems = 0; }); try { // Phase 1: Songs synchronisieren _updateSync('Vergleiche Songs...', 0.1); final serverSongs = await widget.cloud.listSongs(); final db = DbHelper(); final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) await dir.create(recursive: true); int downloaded = 0; int totalNew = 0; // Zähle neue Songs for (final song in serverSongs) { final sid = song['id']?.toString() ?? ''; if (sid.isEmpty) continue; final existing = await db.songNachCloudId(sid); if (existing == null) totalNew++; } // Downloade neue Songs int processed = 0; for (final song in serverSongs) { final sid = song['id']?.toString() ?? ''; if (sid.isEmpty) continue; final title = (song['title'] ?? 'unknown').toString(); final existing = await db.songNachCloudId(sid); if (existing != null) { processed++; continue; } _updateSync('Download: $title...', 0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1))); final safeTitle = sanitizeDateiname(title); final dest = '${dir.path}/$safeTitle'; if (await widget.cloud.download(sid, dest)) { downloaded++; // In DB eintragen mit cloud_id // (vereinfacht: ID3-Reader würde Titel extrahieren) } processed++; _syncedItems = downloaded; await Future.delayed( const Duration(milliseconds: 50)); // UI-Update erlauben } // Phase 2: Favoriten syncen _updateSync('Synchronisiere Favoriten...', 0.55); final favs = await widget.cloud.getFavorites(); final favIds = favs .map((f) => f['id']?.toString() ?? '') .where((id) => id.isNotEmpty) .toList(); await widget.cloud.syncFavorites(favIds); await db.serverFavoritesSet(favIds); setState(() => _favServerCount = favIds.length); _syncedItems += favIds.length; // Phase 3: Playlisten abgleichen _updateSync('Lade Playlisten...', 0.7); await _ladePlaylists(); // Phase 4: Sync-Metadaten aktualisieren _updateSync('Speichere Sync-Zeitpunkt...', 0.9); await widget.cloud.syncAll(); final now = DateTime.now().toIso8601String(); await db.syncMetaSet('last_full_sync', now); _updateSync('Fertig!', 1.0); if (mounted) { setState(() { _syncedItems = downloaded + favIds.length; }); _setzeStatus( '$downloaded Songs + ${favIds.length} Favoriten synchronisiert', ok: true); await _ladeStatus(); } MeloLogger().aktion('cloud_sync_all', { 'downloaded': downloaded, 'favorites': favIds.length, 'playlists': _serverPlaylists.length, }); } catch (e) { MeloLogger().fehler('cloud_sync_all', e); _setzeStatus('Sync-Fehler: $e', ok: false); } finally { if (mounted) { setState(() { _syncLaeuft = false; _syncPhase = ''; }); } } } void _updateSync(String phase, double progress) { if (mounted) { setState(() { _syncPhase = phase; _syncFortschritt = progress; }); } } // ─── 📋 Playlists ─── Future _ladePlaylists() async { setState(() => _ladtPlaylists = true); try { final pls = await widget.cloud.getPlaylists(); if (mounted) { setState(() { _serverPlaylists = pls; _playlistServerCount = pls.length; }); } } catch (e) { MeloLogger().fehler('cloud_playlists_laden', e); } finally { if (mounted) setState(() => _ladtPlaylists = false); } } Future _playlistErstellen() async { final ctrl = TextEditingController(); final name = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Neue Playlist', style: TextStyle(color: Colors.white)), content: TextField( controller: ctrl, autofocus: true, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'Playlist-Name', hintStyle: const TextStyle(color: MeloTheme.textSekundaer), fillColor: MeloTheme.dunkel2, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen'), ), TextButton( onPressed: () => Navigator.pop(ctx, ctrl.text.trim()), child: const Text('Erstellen'), ), ], ), ); if (name != null && name.isNotEmpty) { final result = await widget.cloud.createPlaylist(name); if (result != null) { _setzeStatus('Playlist "$name" erstellt', ok: true); await _ladePlaylists(); } } } Future _playlistLoeschen(int id, String name) async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Playlist löschen?', style: TextStyle(color: Colors.white)), content: Text('"$name" wirklich löschen?', style: const TextStyle(color: MeloTheme.textSekundaer)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Abbrechen')), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Löschen', style: TextStyle(color: MeloTheme.rot)), ), ], ), ); if (ok == true) { await widget.cloud.deletePlaylist(id); await _ladePlaylists(); _setzeStatus('Playlist gelöscht', ok: true); } } // ─── ⭐ Favoriten ─── Future _ladeFavorites() async { setState(() => _ladtFavorites = true); try { final favs = await widget.cloud.getFavorites(); if (mounted) { setState(() { _serverFavorites = favs; _favServerCount = favs.length; }); } } catch (e) { MeloLogger().fehler('cloud_favorites_laden', e); } finally { if (mounted) setState(() => _ladtFavorites = false); } } // ─── Upload / Download ─── static const String _cloudChannelId = 'de.baka.melo.downloads'; static const int _cloudNotifyId = 200; 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')); final fileList = files.toList(); int count = 0; for (int i = 0; i < fileList.length; i++) { final f = fileList[i]; _setzeStatus('Upload: ${f.path.split('/').last}...'); // Progress-Notification if (Platform.isAndroid) { notificationsPlugin.show( id: _cloudNotifyId, title: 'Melo Cloud Upload', body: '${i + 1} / ${fileList.length}: ${f.path.split('/').last}', notificationDetails: NotificationDetails( android: AndroidNotificationDetails( _cloudChannelId, 'Melo Downloads', channelDescription: 'Cloud Upload-Fortschritt', importance: Importance.low, priority: Priority.low, onlyAlertOnce: true, showProgress: true, maxProgress: fileList.length, progress: i, ongoing: true, autoCancel: false, ), ), ); } final sid = await widget.cloud.upload(f.path, f.path.split('/').last); if (sid != null) count++; } // Abschluss-Notification if (Platform.isAndroid) { notificationsPlugin.cancel(id: _cloudNotifyId); final ok = count > 0; notificationsPlugin.show( id: _cloudNotifyId + 1, title: ok ? 'Cloud Upload fertig' : 'Cloud Upload fehlgeschlagen', body: ok ? '$count / ${fileList.length} Songs hochgeladen ✅' : 'Kein Song konnte hochgeladen werden ❌', notificationDetails: NotificationDetails( android: AndroidNotificationDetails( _cloudChannelId, 'Melo Downloads', channelDescription: 'Cloud Upload-Abschluss', importance: Importance.defaultImportance, priority: Priority.defaultPriority, autoCancel: true, ), ), ); } 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 (Platform.isAndroid) { notificationsPlugin.cancel(id: _cloudNotifyId); } if (mounted) { setState(() { _ladt = false; _setzeStatus('Fehler beim Upload', ok: false); }); } } } Future _download() async { if (_syncLaeuft) return; await _syncAlles(); final p = await SharedPreferences.getInstance(); final now = DateTime.now(); final zeit = _formatZeit(now); await p.setString('cloud_last_sync', zeit); await p.setString('cloud_last_sync_ts', now.toIso8601String()); if (mounted) { setState(() => _letzterSync = zeit); _berechneNaechstenSync(); } } // ─── Status ─── Future _ladeStatus() async { final st = await widget.cloud.status(); final syncSt = await widget.cloud.syncStatus(); if (!mounted) return; setState(() { _serverCount = st?['total'] ?? 0; _verbunden = st != null; _statusOk = st != null; _status = st != null ? 'Verbunden' : 'Keine Verbindung (Token?)'; if (syncSt != null) { final counts = syncSt['counts']; if (counts != null) { _favServerCount = counts['favorites'] ?? _favServerCount; _playlistServerCount = counts['playlists'] ?? _playlistServerCount; } } }); } 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); } } void _setzeStatus(String msg, {bool ok = false}) { if (mounted) { setState(() { _status = msg; _statusOk = ok; }); } } // ─── ✏️ Rename Dialog ─── Future _renameDialog(Map song) async { _renameTitleCtrl.text = song['title']?.toString() ?? ''; _renameArtistCtrl.text = song['artist']?.toString() ?? ''; final sid = song['id']?.toString() ?? ''; final result = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Song umbenennen', style: TextStyle(color: Colors.white)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _renameTitleCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: 'Titel', labelStyle: const TextStyle(color: MeloTheme.textSekundaer), fillColor: MeloTheme.dunkel2, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), const SizedBox(height: 12), TextField( controller: _renameArtistCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: 'Künstler', labelStyle: const TextStyle(color: MeloTheme.textSekundaer), fillColor: MeloTheme.dunkel2, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Abbrechen')), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Speichern'), ), ], ), ); if (result == true && sid.isNotEmpty) { final re = await widget.cloud.renameSong(sid, title: _renameTitleCtrl.text.trim(), artist: _renameArtistCtrl.text.trim()); if (re != null && re['status'] == 'ok') { _setzeStatus('Song umbenannt', ok: true); // Auch lokal in DB updaten final db = DbHelper(); final localSong = await db.songNachCloudId(sid); if (localSong?.id != null) { await db.cloudMetadatenAktualisieren(localSong!.id!, title: _renameTitleCtrl.text.trim(), artist: _renameArtistCtrl.text.trim()); } } } } // ─── 🎨 UI ─── @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: () { if (widget.onZurueck != null) { widget.onZurueck!(); } else { 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: () async { await _ladeStatus(); await _ladeServerDaten(); }, ), ], ), body: _syncLaeuft ? _syncFortschrittWidget() : _contentWidget(), ); } Widget _syncFortschrittWidget() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( value: _syncFortschritt > 0 ? _syncFortschritt : null, color: MeloTheme.rot, strokeWidth: 4, ), ), const SizedBox(height: 20), Text( _syncPhase, style: const TextStyle(color: Colors.white, fontSize: 16), ), const SizedBox(height: 8), Text( '${(_syncFortschritt * 100).toInt()}%', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), if (_syncedItems > 0) ...[ const SizedBox(height: 4), Text( '$_syncedItems Elemente synchronisiert', style: const TextStyle( color: Color(0xFFA5D6A7), fontSize: 12), ), ], const SizedBox(height: 24), TextButton( onPressed: () { // Abbruch — läuft im Hintergrund weiter, UI refreshed setState(() => _syncLaeuft = false); }, child: const Text('Im Hintergrund fortsetzen', style: TextStyle(color: MeloTheme.textSekundaer)), ), ], ), ); } Widget _contentWidget() { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ─── Status-Karte ─── _statusKarte(), const SizedBox(height: 20), // ─── Sync-Modus ─── _sektionsHeader('🔄 Sync-Modus'), const SizedBox(height: 8), _syncModusAuswahl(), const SizedBox(height: 16), // ─── 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.sync_rounded, label: 'Jetzt Syncen', beschreibung: _autoSync ? 'Sofort synchronisieren' : 'Manuell syncen', 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-Info ─── _sektionsHeader('📊 Sync-Info'), const SizedBox(height: 8), _syncInfoKarte(), const SizedBox(height: 20), // ─── Server-Playlisten ─── _sektionsHeader('📋 Server-Playlisten'), const SizedBox(height: 8), _playlistSektion(), const SizedBox(height: 20), // ─── Server-Favoriten ─── _sektionsHeader('⭐ Server-Favoriten'), const SizedBox(height: 8), _favoritenSektion(), const SizedBox(height: 20), // ─── Korrupte Songs ─── _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: Column( children: [ 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), ), ], ), if (_naechsterSync != null) ...[ const SizedBox(height: 6), Row( children: [ const Icon(Icons.schedule, color: MeloTheme.textSekundaer, size: 16), const SizedBox(width: 10), const Text('Nächster Sync: ', style: TextStyle( color: MeloTheme.textSekundaer, fontSize: 12)), Text( _naechsterSync!, style: const TextStyle( color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w500), ), ], ), ], ], ), ), const SizedBox(height: 32), ], ), ); } // ─── Widget-Bausteine ─── 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: _verbunden ? const Color(0xFF4CAF50) : const Color(0xFFEF5350), boxShadow: [ BoxShadow( color: (_verbunden ? const Color(0xFF4CAF50) : const Color(0xFFEF5350)) .withValues(alpha: 0.5), blurRadius: 8, ), ], ), ), ], ), ); } Widget _syncModusAuswahl() { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Manuell / Auto Toggle Row( children: [ Expanded(child: _syncModusButton('Manuell', false, Icons.touch_app, !_autoSync)), const SizedBox(width: 4), Expanded(child: _syncModusButton('Auto', true, Icons.sync, _autoSync)), ], ), // Intervall-Auswahl (nur wenn Auto aktiv) if (_autoSync) ...[ const SizedBox(height: 4), const Padding( padding: EdgeInsets.only(left: 8, top: 4), child: Text('Intervall', style: TextStyle( color: MeloTheme.textSekundaer, fontSize: 11, fontWeight: FontWeight.w500)), ), const SizedBox(height: 4), Row( children: [3, 6, 12].map((h) { final aktiv = _syncIntervall == h; return Expanded( child: Padding( padding: EdgeInsets.only( right: h != 12 ? 4 : 0), child: GestureDetector( onTap: () async { setState(() => _syncIntervall = h); (await SharedPreferences.getInstance()) .setInt('cloud_interval', h); _starteAutoSync(); }, child: Container( padding: const EdgeInsets.symmetric( vertical: 10), decoration: BoxDecoration( color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( '${h}h', style: TextStyle( color: aktiv ? Colors.white : MeloTheme.textSekundaer, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ), ), ), ); }).toList(), ), ], ], ), ); } Widget _syncModusButton( String label, bool auto, IconData icon, bool aktiv) { return GestureDetector( onTap: () async { if (auto) { setState(() => _autoSync = true); (await SharedPreferences.getInstance()) .setBool('cloud_auto', true); _starteAutoSync(); _berechneNaechstenSync(); } else { setState(() => _autoSync = false); (await SharedPreferences.getInstance()) .setBool('cloud_auto', false); } }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: aktiv ? MeloTheme.rot : Colors.transparent, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 14, color: aktiv ? Colors.white : MeloTheme.textSekundaer), const SizedBox(width: 6), Text( label, style: TextStyle( color: aktiv ? Colors.white : MeloTheme.textSekundaer, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ), ); } Widget _syncInfoKarte() { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( children: [ _syncInfoZeile('Songs auf Server', '$_serverCount'), _syncInfoZeile('Server-Favoriten', '$_favServerCount'), _syncInfoZeile('Server-Playlisten', '$_playlistServerCount'), _syncInfoZeile('Sync-Modus', _autoSync ? 'Automatisch (${_syncIntervall}h)' : 'Manuell'), ], ), ); } Widget _syncInfoZeile(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 13)), Text(value, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500)), ], ), ); } Widget _playlistSektion() { 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: [ // "Neu" Button Row( children: [ Expanded( child: GestureDetector( onTap: _ladtPlaylists ? null : _playlistErstellen, child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), decoration: BoxDecoration( color: MeloTheme.rot.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add, color: MeloTheme.rot, size: 16), SizedBox(width: 6), Text('Neue Playlist', style: TextStyle( color: MeloTheme.rot, fontSize: 13, fontWeight: FontWeight.w600)), ], ), ), ), ), const SizedBox(width: 8), GestureDetector( onTap: _ladtPlaylists ? null : _ladePlaylists, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: _ladtPlaylists ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: MeloTheme.rot)) : const Icon(Icons.refresh, color: MeloTheme.textSekundaer, size: 16), ), ), ], ), if (_serverPlaylists.isNotEmpty) ...[ const SizedBox(height: 10), const Divider(color: MeloTheme.dunkel2, height: 1), const SizedBox(height: 8), ..._serverPlaylists.map((pl) => _playlistTile(pl)), ] else if (!_ladtPlaylists) ...[ const SizedBox(height: 10), const Text('Keine Playlisten auf dem Server', style: TextStyle( color: MeloTheme.textSekundaer, fontSize: 12)), ], ], ), ); } Widget _playlistTile(Map pl) { final name = pl['name']?.toString() ?? '?'; final count = pl['song_count'] ?? 0; final id = pl['id'] as int? ?? 0; return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ const Icon(Icons.playlist_play, color: MeloTheme.rot, size: 18), const SizedBox(width: 10), Expanded( child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 13)), ), Text('$count Songs', style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11)), const SizedBox(width: 8), GestureDetector( onTap: () => _playlistLoeschen(id, name), child: const Icon(Icons.delete_outline, color: MeloTheme.textSekundaer, size: 16), ), ], ), ); } Widget _favoritenSektion() { 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: [ Row( children: [ const Icon(Icons.favorite, color: MeloTheme.rot, size: 16), const SizedBox(width: 8), Text( '$_favServerCount Favoriten auf dem Server', style: const TextStyle(color: Colors.white, fontSize: 13), ), const Spacer(), GestureDetector( onTap: _ladtFavorites ? null : _ladeFavorites, child: _ladtFavorites ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: MeloTheme.rot)) : const Icon(Icons.refresh, color: MeloTheme.textSekundaer, size: 16), ), ], ), if (_serverFavorites.isNotEmpty) ...[ const SizedBox(height: 8), const Divider(color: MeloTheme.dunkel2, height: 1), const SizedBox(height: 8), ..._serverFavorites.take(5).map((f) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ const Icon(Icons.music_note, color: MeloTheme.textSekundaer, size: 14), const SizedBox(width: 8), Expanded( child: Text( f['title']?.toString() ?? '?', style: const TextStyle( color: Colors.white70, fontSize: 12), ), ), // Rename Button GestureDetector( onTap: () => _renameDialog(f), child: const Icon(Icons.edit, color: MeloTheme.textSekundaer, size: 14), ), ], ), )), if (_serverFavorites.length > 5) Text( '... und ${_serverFavorites.length - 5} weitere', style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11), ), ], ], ), ); } 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: [ 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, ), ), ], ), ), ), ), ], ), 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), ), ), ], ), ); } 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 _sektionsHeader(String titel) { return Text( titel, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 1.2, ), ); } }