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 15993b1d0a v2.35 — Vollständiges Sync-System: Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen
## Server (melo_cloud.py v3.0.0)
- Neue DB-Tabellen: user_playlists, user_playlist_songs, user_favorites, user_history, sync_meta
- Playlist-Endpunkte: GET/POST /api/cloud/playlists, GET /api/cloud/playlists/<id>, POST songs, DELETE songs, PUT positions
- Favoriten-Endpunkte: GET/POST /api/cloud/favorites, POST /api/cloud/favorites/toggle
- History-Endpunkte: GET/POST /api/cloud/history (mit Dedup)
- Rename-Endpunkt: POST /api/cloud/rename (custom_title/custom_artist)
- Sync-Endpunkte: POST /api/cloud/sync-all, GET /api/cloud/sync-status
- delete-all erweitert um Playlists + Favorites + History

## App
- db_helper.dart: Migration v4→v5 (cloud_id, cloud_title, cloud_artist, server_favorites, sync_metadata)
- auth_service.dart: Token-Validierung beim Start (online check + Offline-Fallback), Persistent Login
- cloud_service.dart: Neue Methoden für Playlists (CRUD), Favorites (get/sync/toggle), Rename, History, syncAll/syncStatus
- cloud_screen.dart: Komplett-Rewrite mit Sync-Modus (Manuell/Auto), Intervall-Auswahl, Sync-Fortschritt-Anzeige, Playlist-Verwaltung, Favoriten-Anzeige + Umbenennen, Sync-Info-Karte, nächster Sync
- main.dart: Auto-Sync beim App-Start (nur wenn konfiguriert + fällig)

## Flutter Analyze
- 0 neue Issues — nur 7 pre-existing
2026-08-01 19:32:35 +02:00

1417 lines
45 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 '../utils/farb_theme.dart';
import '../services/cloud_service.dart';
import '../services/auth_service.dart';
import '../database/db_helper.dart';
import '../services/melo_logger.dart';
/// Melo Cloud Sync Screen v3 — vollständiges Sync-System
/// Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen
class CloudScreen extends StatefulWidget {
final CloudService cloud;
const CloudScreen({super.key, required this.cloud});
@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 dest = '${dir.path}/$title';
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 ───
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'));
int count = 0;
for (final f in files) {
_setzeStatus('Upload: ${f.path.split('/').last}...');
final sid = await widget.cloud.upload(f.path, f.path.split('/').last);
if (sid != null) count++;
}
await _ladeStatus();
if (mounted) {
setState(() => _ladt = false);
_setzeStatus('$count Songs hochgeladen', ok: count > 0);
MeloLogger().aktion('cloud_upload', {'count': count});
}
} catch (e) {
MeloLogger().fehler('cloud_upload_path', e);
if (mounted) {
setState(() {
_ladt = false;
_setzeStatus('Fehler beim Upload', ok: false);
});
}
}
}
Future<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: () => 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,
),
);
}
}