CRITICAL: - Logout: LoginScreen statt MeloHome pushen (home_screen.dart) - Path Traversal: sanitizeDateiname() zentral + sync-loop abgesichert - SecureStorage: flutter_secure_storage für Token+Passwort (auth_service, navidrome) HIGH: - Seek: PlayerService.seek() reload-frei (audio_handler) - Play/Pause: Zielzustand statt Toggle für System-Controls - X-User: Header entfernt, Server verlässt sich auf JWT
1481 lines
47 KiB
Dart
1481 lines
47 KiB
Dart
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<CloudScreen> createState() => _CloudScreenState();
|
|
}
|
|
|
|
class _CloudScreenState extends State<CloudScreen> {
|
|
// ─── 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<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();
|
|
_status = 'Verbinde...';
|
|
_verbindeCloud();
|
|
_ladeStatus();
|
|
_ladeSettings();
|
|
_ladeServerDaten();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_syncTimer?.cancel();
|
|
_renameTitleCtrl.dispose();
|
|
_renameArtistCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// ─── Verbindung ───
|
|
|
|
Future<void> _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<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 (!_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<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;
|
|
});
|
|
|
|
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<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'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
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.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<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 _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,
|
|
),
|
|
);
|
|
}
|
|
}
|