import 'dart:async'; import 'dart:convert'; 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 '../services/cloud_service.dart'; import '../services/sync_service.dart'; import '../database/db_helper.dart'; import '../services/melo_logger.dart'; import '../models/song.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 with SingleTickerProviderStateMixin { // ─── Status ─── int _serverCount = 0; int _favServerCount = 0; int _playlistServerCount = 0; Map? _serverStatusDaten; bool _ladt = false; String? _status; bool _statusOk = false; bool _serverDatenGeladen = false; // ─── Sync-Animation ─── late final AnimationController _syncAnimController; // ─── Sync-Einstellungen (F3: Aus/1/3/6/12h, persistiert) ─── /// 0 = Aus (nur manuell). Wird via `cloud_interval` persistiert. int _syncIntervall = 0; String _letzterSync = 'Nie'; String? _letzterSyncTs; String? _naechsterSync; // ─── Sync-Fortschritt ─── String _syncPhase = ''; double _syncFortschritt = 0; /// UI-Flag: Fortschritts-Ansicht anzeigen („Im Hintergrund fortsetzen“ /// setzt NUR dieses Flag zurück — der Sync-Loop läuft im SyncService /// weiter, dessen globaler Guard verhindert Parallel-Syncs). bool _syncLaeuft = false; int _syncedItems = 0; int _syncGesamt = 0; /// Zentraler Sync-Loop (F3: läuft auch ohne geöffneten Tab weiter, /// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls). late final SyncService _sync; // ─── Sync-Historie (heute) ─── List> _syncHistorie = []; // ─── 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(); _syncAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), ); // Zentraler Sync-Service (F3): läuft auch ohne geöffneten Tab weiter, // persistente Notification + Abschluss-Benachrichtigung + Chip-Puls. _sync = SyncService(widget.cloud); _sync.onFortschritt = _updateSync; _sync.onFortschrittZaehler = (aktuell, gesamt) { if (mounted) { setState(() { _syncedItems = aktuell; _syncGesamt = gesamt; }); } }; _sync.onStatus = _setzeStatus; _sync.onHistorie = (dateien, favoriten, playlists) => _syncHistorieEintragen( dateien: dateien, favoriten: favoriten, playlists: playlists); _sync.onKonflikt = _konfliktDialog; _sync.onFavoritenAnzahl = (anzahl) { // NEU-1-Fix: mounted-Guard (Loop läuft evtl. ohne geöffneten Tab) if (mounted) setState(() => _favServerCount = anzahl); }; _sync.onPlaylistenLaden = () async { await _ladePlaylists(); return _serverPlaylists.length; }; _sync.onNachSync = () async { await _ladeStatus(); await _ladeServerDaten(); _berechneNaechstenSync(); }; _sync.onSyncEnde = _syncEnde; _sync.onBericht = _zeigeKonfliktReport; // Verbindung stellt der CloudService beim App-Start her (MeloHome.initState). // Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist. _ladeSettings(); _ladeSyncHistorie(); if (widget.cloud.istVerbunden) { _serverDatenGeladen = true; _ladeServerDaten(); _ladeStatus(); } else if (!widget.cloud.verbindungGestartet) { // Fallback: Service wurde noch nicht gestartet (z.B. eigener Service // aus den Einstellungen) — Verbindung hier anstoßen. widget.cloud.verbinde(); } widget.cloud.addListener(_onCloudStatus); } @override void dispose() { widget.cloud.removeListener(_onCloudStatus); _syncAnimController.dispose(); _renameTitleCtrl.dispose(); _renameArtistCtrl.dispose(); super.dispose(); } // ─── Verbindung ─── /// Reagiert auf Status-Änderungen des CloudService: sobald verbunden, /// werden Serverdaten + Zähler nachgeladen. void _onCloudStatus() { if (!mounted) return; if (widget.cloud.istVerbunden && !_serverDatenGeladen) { _serverDatenGeladen = true; _ladeServerDaten(); _ladeStatus(); } } Future _ladeSettings() async { final p = await SharedPreferences.getInstance(); final letzter = p.getString('cloud_last_sync'); final letzterTs = p.getString('cloud_last_sync_ts'); // F3: cloud_interval (0 = Aus). Migration von cloud_auto (alt: bool). var intervall = p.getInt('cloud_interval'); if (intervall == null) { intervall = (p.getBool('cloud_auto') ?? true) ? 6 : 0; await p.setInt('cloud_interval', intervall); } if (mounted) { setState(() { _syncIntervall = intervall ?? 0; _letzterSync = letzter ?? (letzterTs != null ? _formatZeit(DateTime.tryParse(letzterTs)) : 'Nie'); _letzterSyncTs = letzterTs; }); } _berechneNaechstenSync(); } Future _ladeServerDaten() async { if (!widget.cloud.istVerbunden) return; // Paralleles Laden await Future.wait([ _ladePlaylists(), _ladeFavorites(), ]); } void _berechneNaechstenSync() { if (_syncIntervall == 0) { _naechsterSync = null; return; } final now = DateTime.now(); final last = _letzterSyncTs != null ? DateTime.tryParse(_letzterSyncTs!) : null; if (last != null) { final next = last.add(Duration(hours: _syncIntervall)); if (next.isBefore(now)) { _naechsterSync = 'Jetzt fällig'; } else { _naechsterSync = _formatZeit(next); } } else { _naechsterSync = null; } } String _formatZeit(DateTime? dt) { if (dt == null) return 'Nie'; return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } /// Intervall-Auswahl persistieren + App-weiten Auto-Sync-Timer neu starten. Future _intervallSetzen(int stunden) async { setState(() => _syncIntervall = stunden); final p = await SharedPreferences.getInstance(); await p.setInt('cloud_interval', stunden); await p.setBool('cloud_auto', stunden > 0); // Kompatibilität (altes Flag) await SyncService.starteAutoSyncTimer(); _berechneNaechstenSync(); } // ─── 🔄 Komplett-Sync ─── /// Komplett-Sync (F3): delegiert an den zentralen [SyncService], der auch /// ohne geöffneten Cloud-Tab weiterläuft (persistente Notification, /// Abschluss-Benachrichtigung, ☁️-Chip-Puls, globaler Doppel-Sync-Guard). Future _syncAlles({bool automatisch = false}) async { if (SyncService.laeuftGlobal) return false; if (mounted) { setState(() { _syncLaeuft = true; _syncPhase = 'Verbinde...'; _syncFortschritt = 0; _syncedItems = 0; _syncGesamt = 0; }); } _syncAnimController.repeat(); return _sync.syncAlles(automatisch: automatisch); } /// Immer am Loop-Ende (auch bei Fehler / nach „Im Hintergrund fortsetzen“): /// Animation stoppen, Sync-Ansicht schließen. Controller-Zugriffe abgesichert, /// falls der Screen während des Hintergrund-Syncs disposed wurde (MED-4). void _syncEnde() { try { _syncAnimController.stop(); _syncAnimController.value = 0; } catch (_) { // Controller kann bereits disposed sein (Screen verlassen) } if (mounted) { setState(() { _syncLaeuft = false; _syncPhase = ''; }); } } /// Konflikt-Report (Sprint E): Zusammenfassung nach langem Offline-Sync. /// „Willkommen zurück! Seit letztem Sync: +8 neue · −3 gelöscht · ⭐5“ void _zeigeKonfliktReport(SyncBericht bericht) { if (!mounted) return; // LOW-1: „Willkommen zurück!“ nur zeigen, wenn der letzte Sync mehr als // 24h zurückliegt (oder noch nie gesynct) — sonst Dialog-Flut bei jedem // Sync mit Änderungen. Basis ist der letzterSync-Stand VOR diesem Lauf. if (!bericht.nachLangerPause()) return; final titel = 'Willkommen zurück!'; final zeilen = [ 'Seit deinem letzten Sync:', if (bericht.neueSongs > 0) '➕ ${bericht.neueSongs} neue Songs', if (bericht.geloeschteSongs > 0) '🗑️ ${bericht.geloeschteSongs} gelöscht', if (bericht.favoritenGeaendert > 0) '⭐ ${bericht.favoritenGeaendert} Favoriten geändert', ]; showDialog( context: context, barrierDismissible: true, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: Text(titel, style: const TextStyle(color: Colors.white, fontSize: 18)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: zeilen .map((z) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Text(z, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 14)), )) .toList(), ), ), actions: [ TextButton( onPressed: () { Navigator.pop(ctx); if (bericht.details.isNotEmpty) { _zeigeReportDetails(bericht); } }, child: const Text('Details', style: TextStyle(color: MeloTheme.rot)), ), TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('OK', style: TextStyle(color: Colors.white)), ), ], ), ); } void _zeigeReportDetails(SyncBericht bericht) { if (!mounted) return; showModalBottomSheet( context: context, backgroundColor: MeloTheme.dunkel1, builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Änderungen', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), Flexible( child: ListView( shrinkWrap: true, children: bericht.details .map((d) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text(d, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 13)), )) .toList(), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Schließen', style: TextStyle(color: MeloTheme.rot)), ), ), ], ), ), ), ); } // ─── 📜 Sync-Historie (fürs Dashboard) ─── static const _historieKey = 'cloud_sync_history'; static const _historieCap = 30; Future _ladeSyncHistorie() async { final p = await SharedPreferences.getInstance(); final roh = p.getStringList(_historieKey) ?? []; final eintraege = >[]; for (final s in roh) { try { final m = jsonDecode(s) as Map; eintraege.add(m); } catch (_) { // kaputte Einträge ignorieren } } if (mounted) setState(() => _syncHistorie = eintraege); } Future _syncHistorieEintragen({ required int dateien, required int favoriten, required int playlists, }) async { final eintrag = { 'ts': DateTime.now().toIso8601String(), 'dateien': dateien, 'favoriten': favoriten, 'playlists': playlists, }; final p = await SharedPreferences.getInstance(); final liste = p.getStringList(_historieKey) ?? []; liste.add(jsonEncode(eintrag)); while (liste.length > _historieCap) { liste.removeAt(0); } await p.setStringList(_historieKey, liste); if (mounted) { setState(() => _syncHistorie = [ ..._syncHistorie, eintrag, ]); } } /// Einträge von heute zusammenfassen: „heute 14 Dateien, 2 Favoriten, 1 Playlist“ String get _syncHistorieHeuteText { final heute = DateTime.now(); int dateien = 0; int favoriten = 0; int playlists = 0; for (final e in _syncHistorie) { final ts = DateTime.tryParse(e['ts']?.toString() ?? ''); if (ts == null) continue; if (ts.year == heute.year && ts.month == heute.month && ts.day == heute.day) { dateien += (e['dateien'] as num?)?.toInt() ?? 0; favoriten += (e['favoriten'] as num?)?.toInt() ?? 0; playlists += (e['playlists'] as num?)?.toInt() ?? 0; } } if (dateien == 0 && favoriten == 0 && playlists == 0) { return 'Noch keine Syncs heute'; } final teile = [ if (dateien > 0) '$dateien Dateien', if (favoriten > 0) '$favoriten Favoriten', if (playlists > 0) '$playlists Playlist${playlists != 1 ? 'en' : ''}', ]; return 'Heute: ${teile.join(', ')}'; } /// Konflikt-Dialog: Titel ODER Künstler weichen lokal ≠ Server ab. /// Dismissable (Tap außerhalb = Abbruch → lokale Version behalten, der /// Sync läuft weiter). Batch-Optionen: Checkbox „Für alle übernehmen“ + /// Button „Alle weiteren überspringen“. /// Rückgabe: [KonfliktErgebnis] ('lokal' | 'server' | 'beide' | /// 'ueberspringen') oder null bei Abbruch. Die Batch-Logik übernimmt der /// SyncService (MED-3) — der Dialog liefert nur wahl + fuerAlle. Future _konfliktDialog(Song lokal, Map serverSong) async { final serverTitel = (serverSong['title'] ?? '?').toString(); final serverKuenstler = (serverSong['artist'] ?? '?').toString(); if (!mounted) return null; var fuerAlle = false; final wahl = await showDialog( context: context, barrierDismissible: true, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('⚠️ Konflikt', style: TextStyle(color: Colors.white, fontSize: 17)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Titel oder Künstler weichen voneinander ab:', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('📱 Lokal', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), const SizedBox(height: 2), Text(lokal.titel, style: const TextStyle(color: Colors.white, fontSize: 13)), Text(lokal.kuenstler, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), ], ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('☁️ Server', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), const SizedBox(height: 2), Text(serverTitel, style: const TextStyle(color: Colors.white, fontSize: 13)), Text(serverKuenstler, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), ], ), ), const SizedBox(height: 4), CheckboxListTile( value: fuerAlle, onChanged: (v) => setDialogState(() => fuerAlle = v ?? false), title: const Text('Für alle weiteren Konflikte übernehmen', style: TextStyle(color: Colors.white, fontSize: 12)), dense: true, contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, activeColor: MeloTheme.rot, checkColor: Colors.white, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, 'lokal'), child: const Text('Lokale behalten', style: TextStyle(color: MeloTheme.textSekundaer)), ), TextButton( onPressed: () => Navigator.pop(ctx, 'beide'), child: const Text('Beide behalten', style: TextStyle(color: MeloTheme.textSekundaer)), ), TextButton( onPressed: () => Navigator.pop(ctx, 'server'), child: const Text('Server übernehmen', style: TextStyle(color: MeloTheme.rot)), ), TextButton( onPressed: () => Navigator.pop(ctx, 'ueberspringen'), child: const Text('Alle weiteren überspringen', style: TextStyle(color: MeloTheme.textSekundaer)), ), ], ), ), ); if (wahl == null) return null; // 'ueberspringen' = alle weiteren überspringen → fuerAlle=true, damit der // SyncService die Batch-Entscheidung (keep local) übernimmt. return KonfliktErgebnis(wahl, fuerAlle: wahl == 'ueberspringen' || fuerAlle); } void _updateSync(String phase, double progress) { if (mounted) { setState(() { _syncPhase = phase; _syncFortschritt = progress; }); } } // ─── 📋 Playlists ─── Future _ladePlaylists() async { if (!mounted) return; // NEU-1: Loop kann ohne geöffneten Tab laufen 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'), ), ], ), ); ctrl.dispose(); 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 { if (!mounted) return; // NEU-1: Loop kann ohne geöffneten Tab laufen 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 db = DbHelper(); 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++; // Lokalen Song mit dem Server verknüpfen (cloud_id) — Grundlage // für den bidirektionalen Favoriten-Sync (lokal → Server). try { final lokalerSong = await db.songNachPfad(f.path); if (lokalerSong?.id != null) { await db.cloudIdSetzen(lokalerSong!.id!, sid); } } catch (_) { // Verknüpfung ist nice-to-have — Upload selbst war erfolgreich } } } // 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 (SyncService.laeuftGlobal) { _setzeStatus('Sync läuft bereits im Hintergrund', ok: false); return; } final erfolgreich = await _syncAlles(); if (!erfolgreich) return; // SyncService persistiert cloud_last_sync nur bei Erfolg // UI: letzten Sync anzeigen. KEINE zweite cloud_last_sync_ts-Schreibung // mit now-am-Ende hier — SyncService persistiert bereits den // Sync-BEGINN-Snapshot (MED-1: Tombstone-Race). Eine spätere Schreibung // würde die Löschungen zwischen listSongs und Sync-Ende wieder als // „bereits gesehen“ markieren. final p = await SharedPreferences.getInstance(); final letzterTs = p.getString('cloud_last_sync_ts'); if (mounted) { setState(() { _letzterSyncTs = letzterTs; _letzterSync = letzterTs != null ? _formatZeit(DateTime.tryParse(letzterTs) ?? DateTime.now()) : _formatZeit(DateTime.now()); _berechneNaechstenSync(); }); } } // ─── Status ─── Future _ladeStatus() async { final st = await widget.cloud.statusDaten(); final syncSt = await widget.cloud.syncStatus(); if (!mounted) return; setState(() { _serverStatusDaten = st; _serverCount = st?['total'] ?? 0; 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 widget.cloud.verbinde(); await _ladeStatus(); await _ladeServerDaten(); }, ), ], ), // ListenableBuilder: Status kommt live vom CloudService // ('Verbinde...' → 'Verbunden'/'Keine Verbindung') body: ListenableBuilder( listenable: widget.cloud, builder: (context, _) => _syncLaeuft ? _syncFortschrittWidget() : _contentWidget(), ), ); } Widget _syncFortschrittWidget() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 90, height: 90, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 90, height: 90, child: CircularProgressIndicator( value: _syncFortschritt > 0 ? _syncFortschritt : null, color: MeloTheme.rot, strokeWidth: 4, ), ), // Rotierende ☁️-Animation während des Syncs RotationTransition( turns: _syncAnimController, child: Container( width: 44, height: 44, decoration: BoxDecoration( color: MeloTheme.dunkel1, shape: BoxShape.circle, ), child: const Icon(Icons.cloud_sync, color: MeloTheme.rot, size: 26), ), ), ], ), ), 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( _syncGesamt > 0 ? '$_syncedItems / $_syncGesamt synchronisiert' : '$_syncedItems Elemente synchronisiert', style: const TextStyle( color: Color(0xFFA5D6A7), fontSize: 12), ), ], const SizedBox(height: 24), TextButton( onPressed: () { // Nur die UI-Persistenz ändern: Fortschritts-Ansicht schließen. // Der Sync-Loop läuft im SyncService weiter und der globale // Guard (SyncService.laeuftGlobal) bleibt gesperrt, bis der // Loop fertig ist — ein erneuter „Jetzt Syncen“ startet KEINEN // Parallel-Sync. Sichtbar bleibt er via Notification + Chip-Puls. 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: _syncIntervall > 0 ? '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: transiente Meldungen (Upload-Fortschritt, Sync-Ergebnis) // haben Vorrang — sonst kommt der Verbindungsstatus live vom Service // ('Verbinde...' → 'Verbunden' / 'Keine Verbindung') if (!_ladt) ...[ Builder(builder: (context) { final hatTransientStatus = _status != null; final verbindet = widget.cloud.status == CloudStatus.verbinde; final statusOk = hatTransientStatus ? _statusOk : widget.cloud.istVerbunden; return 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) : verbindet ? const Color(0xFF3B2A0D) : const Color(0xFF3B0D0D), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Icon( statusOk ? Icons.check_circle : verbindet ? Icons.sync : Icons.info_outline, size: 16, color: statusOk ? const Color(0xFF4CAF50) : verbindet ? const Color(0xFFFFA726) : const Color(0xFFEF5350), ), const SizedBox(width: 8), Expanded( child: Text( _status ?? widget.cloud.statusText, style: TextStyle( color: statusOk ? const Color(0xFFA5D6A7) : verbindet ? const Color(0xFFFFCC80) : 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 (live vom CloudService) Builder(builder: (context) { final farbe = widget.cloud.istVerbunden ? const Color(0xFF4CAF50) : widget.cloud.status == CloudStatus.verbinde ? const Color(0xFFFFA726) : const Color(0xFFEF5350); return Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: farbe, boxShadow: [ BoxShadow( color: farbe.withValues(alpha: 0.5), blurRadius: 8, ), ], ), ); }), ], ), ); } /// Auto-Sync-Intervall-Auswahl (F3): Aus/1h/3h/6h/12h. /// 0 = Aus (nur manuell) — ersetzt den alten Manuell/Auto-Toggle. Widget _syncModusAuswahl() { const optionen = [0, 1, 3, 6, 12]; 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: [ const Padding( padding: EdgeInsets.only(left: 8, top: 4), child: Text('Auto-Sync-Intervall', style: TextStyle( color: MeloTheme.textSekundaer, fontSize: 11, fontWeight: FontWeight.w500)), ), const SizedBox(height: 4), Row( children: optionen.map((h) { final aktiv = _syncIntervall == h; final label = h == 0 ? 'Aus' : '${h}h'; return Expanded( child: Padding( padding: EdgeInsets.only(right: h != 12 ? 4 : 0), child: GestureDetector( onTap: () => _intervallSetzen(h), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( label, style: TextStyle( color: aktiv ? Colors.white : MeloTheme.textSekundaer, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ), ), ), ); }).toList(), ), ], ), ); } 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Statistik-Karten 2×2: Songs · Playlisten · Favoriten · Letzter Sync ── Row( children: [ Expanded( child: _statistikKarte( icon: Icons.library_music, label: 'Songs', wert: '$_serverCount', ), ), const SizedBox(width: 10), Expanded( child: _statistikKarte( icon: Icons.playlist_play, label: 'Playlisten', wert: '$_playlistServerCount', ), ), ], ), const SizedBox(height: 10), Row( children: [ Expanded( child: _statistikKarte( icon: Icons.favorite, label: 'Favoriten', wert: '$_favServerCount', ), ), const SizedBox(width: 10), Expanded( child: _statistikKarte( icon: Icons.history, label: 'Letzter Sync', wert: _letzterSync, ), ), ], ), const SizedBox(height: 14), const Divider(color: MeloTheme.dunkel2, height: 1), const SizedBox(height: 12), // ── Speicheranzeige (Balken, falls Server Daten liefert) ── _speicherZeile(), const SizedBox(height: 12), // ── Sync-Historie (heute) ── _syncHistorieZeile(), ], ), ); } Widget _statistikKarte({ required IconData icon, required String label, required String wert, }) { return Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: MeloTheme.rot, size: 16), const SizedBox(height: 8), Text( wert, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( label, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11), ), ], ), ); } /// Speicheranzeige mit Balken. Liefert der Server keine Storage-Daten /// (Felder `storage_used`/`storage_total` o.ä.), erscheint Platzhalter „–". Widget _speicherZeile() { final st = _serverStatusDaten; num? used; num? total; if (st != null) { used = (st['storage_used'] ?? st['bytes_used'] ?? st['used_bytes']) as num?; total = (st['storage_total'] ?? st['bytes_total'] ?? st['total_bytes']) as num?; } final hatDaten = used != null && total != null && total > 0; final anteil = hatDaten ? (used / total).clamp(0.0, 1.0) : 0.0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.storage, color: MeloTheme.textSekundaer, size: 16), const SizedBox(width: 8), const Text('Speicher', style: TextStyle( color: MeloTheme.textSekundaer, fontSize: 13)), const Spacer(), Text( hatDaten ? '${_formatBytes(used)} / ${_formatBytes(total)}' : '–', style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: hatDaten ? anteil.toDouble() : 0, minHeight: 6, backgroundColor: MeloTheme.dunkel2, valueColor: const AlwaysStoppedAnimation(MeloTheme.rot), ), ), ], ); } String _formatBytes(num bytes) { if (bytes <= 0) return '0 B'; const einheiten = ['B', 'KB', 'MB', 'GB', 'TB']; var wert = bytes.toDouble(); var i = 0; while (wert >= 1024 && i < einheiten.length - 1) { wert /= 1024; i++; } return '${wert.toStringAsFixed(wert >= 100 ? 0 : 1)} ${einheiten[i]}'; } /// Sync-Historie-Zeile: „Heute: 14 Dateien, 2 Favoriten, 1 Playlist“ Widget _syncHistorieZeile() { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.event_note, color: MeloTheme.textSekundaer, size: 16), const SizedBox(width: 8), Expanded( child: Text( _syncHistorieHeuteText, style: const TextStyle(color: Colors.white70, fontSize: 12), ), ), ], ); } 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, ), ); } }