This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/screens/cloud_screen.dart
T
Dustin eb379c7790 v2.51.3 — Cloud-Dashboard UI (Statistik-Karten, Speicher, Sync-Historie) + Lint-Fixes
## Cloud-Dashboard (cloud_screen.dart)
- Statistik-Karten 2x2: Songs, Playlisten, Favoriten, Letzter Sync (Daten aus statusDaten/syncStatus)
- Speicheranzeige mit Balken — Platzhalter „–" wenn der Server keine Storage-Daten liefert (storage_used/storage_total o.ä.)
- Sync-Historie-Zeile: „Heute: X Dateien, Y Favoriten, Z Playlisten" aus _syncHistorie (heute gefiltert)
- Konflikt-Dialog NUR bei abweichendem Titel (lokal ≠ Server), sonst kein Dialog
- Lint-Fixes: curly_braces (if ohne Block) + unused _syncHistorieHeuteText (jetzt in der Historie-Zeile verdrahtet)
- Sync-Animation während des Syncs: rotierendes ☁️-Icon + Fortschrittsring (CloudStatus/AnimationController)

Keine neuen Packages, flutter analyze 0 Issues, Tests 98/98 grün
2026-08-05 08:02:36 +02:00

1898 lines
62 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 '../utils/sanitize.dart';
import '../services/cloud_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<CloudScreen> createState() => _CloudScreenState();
}
class _CloudScreenState extends State<CloudScreen>
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 ───
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;
// ─── Sync-Historie (heute) ───
List<Map<String, dynamic>> _syncHistorie = [];
// ─── Korrupt ───
List<Map> _korrupteSongs = [];
bool _ladtKorrupt = false;
bool _hatGeprueft = false;
// ─── Server-Playlisten ───
List<Map> _serverPlaylists = [];
bool _ladtPlaylists = false;
// ─── Server-Favoriten ───
List<Map> _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),
);
// 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);
_syncTimer?.cancel();
_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<void> _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<void> _ladeServerDaten() async {
if (!widget.cloud.istVerbunden) 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<void> _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<void> _syncAlles() async {
if (_syncLaeuft) return;
setState(() {
_syncLaeuft = true;
_syncPhase = 'Verbinde...';
_syncFortschritt = 0;
_syncedItems = 0;
});
_syncAnimController.repeat();
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 + löse Konflikte (Titel lokal ≠ Server)
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) {
// Konfliktprüfung: Server-Titel weicht vom lokalen ab
final serverTitel = title.trim();
final lokalerTitel = existing.titel.trim();
if (serverTitel.isNotEmpty &&
lokalerTitel.toLowerCase() != serverTitel.toLowerCase()) {
final wahl = await _konfliktDialog(existing, song);
if (wahl == 'server') {
await db.cloudMetadatenAktualisieren(
existing.id!,
title: serverTitel,
artist: (song['artist']?.toString() ?? '').trim(),
);
_syncedItems++;
} else if (wahl == 'beide') {
// Server-Kopie als eigenen lokalen Song ohne cloud_id anlegen
final safeTitle = sanitizeDateiname(serverTitel);
var dest = '${dir.path}/$safeTitle';
if (await File(dest).exists()) {
dest = '${dir.path}/$safeTitle (Server)';
}
if (await widget.cloud.download(sid, dest)) {
await db.songEinfuegen(Song(
titel: serverTitel,
kuenstler: (song['artist']?.toString() ?? '').trim(),
dauerSekunden: 0,
dateiPfad: dest,
downloadQuelle: 'cloud',
istHeruntergeladen: true,
));
downloaded++;
_syncedItems = downloaded;
}
}
// 'lokal' → nichts tun (lokale Version behalten)
}
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);
// Sync-Historie fürs Dashboard festhalten
await _syncHistorieEintragen(
dateien: downloaded,
favoriten: favIds.length,
playlists: _serverPlaylists.length,
);
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 {
_syncAnimController.stop();
_syncAnimController.value = 0;
if (mounted) {
setState(() {
_syncLaeuft = false;
_syncPhase = '';
});
}
}
}
// ─── 📜 Sync-Historie (fürs Dashboard) ───
static const _historieKey = 'cloud_sync_history';
static const _historieCap = 30;
Future<void> _ladeSyncHistorie() async {
final p = await SharedPreferences.getInstance();
final roh = p.getStringList(_historieKey) ?? [];
final eintraege = <Map<String, dynamic>>[];
for (final s in roh) {
try {
final m = jsonDecode(s) as Map<String, dynamic>;
eintraege.add(m);
} catch (_) {
// kaputte Einträge ignorieren
}
}
if (mounted) setState(() => _syncHistorie = eintraege);
}
Future<void> _syncHistorieEintragen({
required int dateien,
required int favoriten,
required int playlists,
}) async {
final eintrag = <String, dynamic>{
'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 = <String>[
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 lokal ≠ Server. NUR wenn ein Konflikt existiert.
/// Rückgabe: 'lokal' | 'server' | 'beide' | null (abgebrochen)
Future<String?> _konfliktDialog(Song lokal, Map serverSong) async {
final serverTitel = (serverSong['title'] ?? '?').toString();
final serverKuenstler = (serverSong['artist'] ?? '?').toString();
if (!mounted) return null;
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (ctx) => 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 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)),
],
),
),
],
),
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)),
),
],
),
);
}
void _updateSync(String phase, double progress) {
if (mounted) {
setState(() {
_syncPhase = phase;
_syncFortschritt = progress;
});
}
}
// ─── 📋 Playlists ───
Future<void> _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<void> _playlistErstellen() async {
final ctrl = TextEditingController();
final name = await showDialog<String>(
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<void> _playlistLoeschen(int id, String name) async {
final ok = await showDialog<bool>(
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<void> _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<void> _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<File>().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<void> _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<void> _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<void> _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<void> _renameDialog(Map song) async {
_renameTitleCtrl.text = song['title']?.toString() ?? '';
_renameArtistCtrl.text = song['artist']?.toString() ?? '';
final sid = song['id']?.toString() ?? '';
final result = await showDialog<bool>(
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(
'$_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: 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,
),
],
),
);
}),
],
),
);
}
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(
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<Color>(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,
),
);
}
}