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
This commit is contained in:
+403
-16
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -9,6 +10,7 @@ import '../utils/sanitize.dart';
|
|||||||
import '../services/cloud_service.dart';
|
import '../services/cloud_service.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
import '../services/melo_logger.dart';
|
import '../services/melo_logger.dart';
|
||||||
|
import '../models/song.dart';
|
||||||
import '../main.dart'; // notificationsPlugin
|
import '../main.dart'; // notificationsPlugin
|
||||||
|
|
||||||
/// Melo Cloud Sync Screen v3 — vollständiges Sync-System
|
/// Melo Cloud Sync Screen v3 — vollständiges Sync-System
|
||||||
@@ -22,16 +24,21 @@ class CloudScreen extends StatefulWidget {
|
|||||||
State<CloudScreen> createState() => _CloudScreenState();
|
State<CloudScreen> createState() => _CloudScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CloudScreenState extends State<CloudScreen> {
|
class _CloudScreenState extends State<CloudScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
// ─── Status ───
|
// ─── Status ───
|
||||||
int _serverCount = 0;
|
int _serverCount = 0;
|
||||||
int _favServerCount = 0;
|
int _favServerCount = 0;
|
||||||
int _playlistServerCount = 0;
|
int _playlistServerCount = 0;
|
||||||
|
Map? _serverStatusDaten;
|
||||||
bool _ladt = false;
|
bool _ladt = false;
|
||||||
String? _status;
|
String? _status;
|
||||||
bool _statusOk = false;
|
bool _statusOk = false;
|
||||||
bool _serverDatenGeladen = false;
|
bool _serverDatenGeladen = false;
|
||||||
|
|
||||||
|
// ─── Sync-Animation ───
|
||||||
|
late final AnimationController _syncAnimController;
|
||||||
|
|
||||||
// ─── Sync-Einstellungen ───
|
// ─── Sync-Einstellungen ───
|
||||||
bool _autoSync = true;
|
bool _autoSync = true;
|
||||||
int _syncIntervall = 6;
|
int _syncIntervall = 6;
|
||||||
@@ -45,6 +52,9 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
bool _syncLaeuft = false;
|
bool _syncLaeuft = false;
|
||||||
int _syncedItems = 0;
|
int _syncedItems = 0;
|
||||||
|
|
||||||
|
// ─── Sync-Historie (heute) ───
|
||||||
|
List<Map<String, dynamic>> _syncHistorie = [];
|
||||||
|
|
||||||
// ─── Korrupt ───
|
// ─── Korrupt ───
|
||||||
List<Map> _korrupteSongs = [];
|
List<Map> _korrupteSongs = [];
|
||||||
bool _ladtKorrupt = false;
|
bool _ladtKorrupt = false;
|
||||||
@@ -65,9 +75,14 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_syncAnimController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
);
|
||||||
// Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
|
// Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
|
||||||
// Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
|
// Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
|
||||||
_ladeSettings();
|
_ladeSettings();
|
||||||
|
_ladeSyncHistorie();
|
||||||
if (widget.cloud.istVerbunden) {
|
if (widget.cloud.istVerbunden) {
|
||||||
_serverDatenGeladen = true;
|
_serverDatenGeladen = true;
|
||||||
_ladeServerDaten();
|
_ladeServerDaten();
|
||||||
@@ -84,6 +99,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
widget.cloud.removeListener(_onCloudStatus);
|
widget.cloud.removeListener(_onCloudStatus);
|
||||||
_syncTimer?.cancel();
|
_syncTimer?.cancel();
|
||||||
|
_syncAnimController.dispose();
|
||||||
_renameTitleCtrl.dispose();
|
_renameTitleCtrl.dispose();
|
||||||
_renameArtistCtrl.dispose();
|
_renameArtistCtrl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -188,6 +204,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
_syncFortschritt = 0;
|
_syncFortschritt = 0;
|
||||||
_syncedItems = 0;
|
_syncedItems = 0;
|
||||||
});
|
});
|
||||||
|
_syncAnimController.repeat();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Phase 1: Songs synchronisieren
|
// Phase 1: Songs synchronisieren
|
||||||
@@ -209,7 +226,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
if (existing == null) totalNew++;
|
if (existing == null) totalNew++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Downloade neue Songs
|
// Downloade neue Songs + löse Konflikte (Titel lokal ≠ Server)
|
||||||
int processed = 0;
|
int processed = 0;
|
||||||
for (final song in serverSongs) {
|
for (final song in serverSongs) {
|
||||||
final sid = song['id']?.toString() ?? '';
|
final sid = song['id']?.toString() ?? '';
|
||||||
@@ -217,6 +234,41 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
final title = (song['title'] ?? 'unknown').toString();
|
final title = (song['title'] ?? 'unknown').toString();
|
||||||
final existing = await db.songNachCloudId(sid);
|
final existing = await db.songNachCloudId(sid);
|
||||||
if (existing != null) {
|
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++;
|
processed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -266,6 +318,12 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
_setzeStatus(
|
_setzeStatus(
|
||||||
'$downloaded Songs + ${favIds.length} Favoriten synchronisiert',
|
'$downloaded Songs + ${favIds.length} Favoriten synchronisiert',
|
||||||
ok: true);
|
ok: true);
|
||||||
|
// Sync-Historie fürs Dashboard festhalten
|
||||||
|
await _syncHistorieEintragen(
|
||||||
|
dateien: downloaded,
|
||||||
|
favoriten: favIds.length,
|
||||||
|
playlists: _serverPlaylists.length,
|
||||||
|
);
|
||||||
await _ladeStatus();
|
await _ladeStatus();
|
||||||
}
|
}
|
||||||
MeloLogger().aktion('cloud_sync_all', {
|
MeloLogger().aktion('cloud_sync_all', {
|
||||||
@@ -277,6 +335,8 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
MeloLogger().fehler('cloud_sync_all', e);
|
MeloLogger().fehler('cloud_sync_all', e);
|
||||||
_setzeStatus('Sync-Fehler: $e', ok: false);
|
_setzeStatus('Sync-Fehler: $e', ok: false);
|
||||||
} finally {
|
} finally {
|
||||||
|
_syncAnimController.stop();
|
||||||
|
_syncAnimController.value = 0;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_syncLaeuft = false;
|
_syncLaeuft = false;
|
||||||
@@ -286,6 +346,166 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 📜 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) {
|
void _updateSync(String phase, double progress) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -521,6 +741,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
final syncSt = await widget.cloud.syncStatus();
|
final syncSt = await widget.cloud.syncStatus();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_serverStatusDaten = st;
|
||||||
_serverCount = st?['total'] ?? 0;
|
_serverCount = st?['total'] ?? 0;
|
||||||
|
|
||||||
if (syncSt != null) {
|
if (syncSt != null) {
|
||||||
@@ -698,14 +919,36 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 80,
|
width: 90,
|
||||||
height: 80,
|
height: 90,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: _syncFortschritt > 0 ? _syncFortschritt : null,
|
value: _syncFortschritt > 0 ? _syncFortschritt : null,
|
||||||
color: MeloTheme.rot,
|
color: MeloTheme.rot,
|
||||||
strokeWidth: 4,
|
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),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
_syncPhase,
|
_syncPhase,
|
||||||
@@ -1135,32 +1378,176 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
border: Border.all(color: MeloTheme.dunkel2),
|
border: Border.all(color: MeloTheme.dunkel2),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_syncInfoZeile('Songs auf Server', '$_serverCount'),
|
// ── Statistik-Karten 2×2: Songs · Playlisten · Favoriten · Letzter Sync ──
|
||||||
_syncInfoZeile('Server-Favoriten', '$_favServerCount'),
|
Row(
|
||||||
_syncInfoZeile('Server-Playlisten', '$_playlistServerCount'),
|
children: [
|
||||||
_syncInfoZeile('Sync-Modus', _autoSync ? 'Automatisch (${_syncIntervall}h)' : 'Manuell'),
|
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 _syncInfoZeile(String label, String value) {
|
Widget _statistikKarte({
|
||||||
return Padding(
|
required IconData icon,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
required String label,
|
||||||
child: Row(
|
required String wert,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
}) {
|
||||||
|
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: [
|
children: [
|
||||||
Text(label,
|
Icon(icon, color: MeloTheme.rot, size: 16),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
wert,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
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)),
|
color: MeloTheme.textSekundaer, fontSize: 13)),
|
||||||
Text(value,
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
hatDaten
|
||||||
|
? '${_formatBytes(used)} / ${_formatBytes(total)}'
|
||||||
|
: '–',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500)),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user