v2.52.2 — SyncService-Zentralisierung (F3): persistente Sync-Notification, Auto-Sync-Intervall, Schritt-Feedback, 12/25-Zähler
This commit is contained in:
+146
-352
@@ -6,10 +6,8 @@ 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/favoriten_service.dart';
|
||||
import '../services/favoriten_sync.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../database/db_helper.dart';
|
||||
import '../services/melo_logger.dart';
|
||||
import '../models/song.dart';
|
||||
@@ -38,35 +36,29 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
bool _statusOk = false;
|
||||
bool _serverDatenGeladen = false;
|
||||
|
||||
// ─── Favoriten (bidirektionaler Sync) ───
|
||||
final FavoritenService _favoriten = FavoritenService();
|
||||
|
||||
// ─── Sync-Animation ───
|
||||
late final AnimationController _syncAnimController;
|
||||
|
||||
// ─── Sync-Einstellungen ───
|
||||
bool _autoSync = true;
|
||||
int _syncIntervall = 6;
|
||||
Timer? _syncTimer;
|
||||
// ─── Sync-Einstellungen (F3: Aus/1/3/6/12h, persistiert) ───
|
||||
/// 0 = Aus (nur manuell). Wird via `cloud_interval` persistiert.
|
||||
int _syncIntervall = 0;
|
||||
String _letzterSync = 'Nie';
|
||||
String? _letzterSyncTs;
|
||||
String? _naechsterSync;
|
||||
|
||||
// ─── Sync-Fortschritt ───
|
||||
String _syncPhase = '';
|
||||
double _syncFortschritt = 0;
|
||||
/// UI-Flag: Fortschritts-Ansicht anzeigen („Im Hintergrund fortsetzen“
|
||||
/// setzt NUR dieses Flag zurück).
|
||||
/// setzt NUR dieses Flag zurück — der Sync-Loop läuft im SyncService
|
||||
/// weiter, dessen globaler Guard verhindert Parallel-Syncs).
|
||||
bool _syncLaeuft = false;
|
||||
/// Echter Sync-Guard: bleibt true, solange der Sync-Loop tatsächlich läuft
|
||||
/// (auch im Hintergrund nach „Im Hintergrund fortsetzen“). Erst nach dem
|
||||
/// echten Loop-Ende in `finally` wird er freigegeben → kein Doppel-Sync.
|
||||
bool _syncProzessLaeuft = false;
|
||||
int _syncedItems = 0;
|
||||
int _syncGesamt = 0;
|
||||
|
||||
// ─── Konflikt-Batch (MED-3) ───
|
||||
/// Batch-Entscheidung für alle weiteren Konflikte dieses Sync-Laufs:
|
||||
/// 'lokal' | 'server' | 'beide' | 'ueberspringen' | null (jeden fragen)
|
||||
String? _konfliktBatch;
|
||||
/// Zentraler Sync-Loop (F3: läuft auch ohne geöffneten Tab weiter,
|
||||
/// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls).
|
||||
late final SyncService _sync;
|
||||
|
||||
// ─── Sync-Historie (heute) ───
|
||||
List<Map<String, dynamic>> _syncHistorie = [];
|
||||
@@ -95,6 +87,37 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
);
|
||||
// Zentraler Sync-Service (F3): läuft auch ohne geöffneten Tab weiter,
|
||||
// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls.
|
||||
_sync = SyncService(widget.cloud);
|
||||
_sync.onFortschritt = _updateSync;
|
||||
_sync.onFortschrittZaehler = (aktuell, gesamt) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_syncedItems = aktuell;
|
||||
_syncGesamt = gesamt;
|
||||
});
|
||||
}
|
||||
};
|
||||
_sync.onStatus = _setzeStatus;
|
||||
_sync.onHistorie = (dateien, favoriten, playlists) =>
|
||||
_syncHistorieEintragen(
|
||||
dateien: dateien, favoriten: favoriten, playlists: playlists);
|
||||
_sync.onKonflikt = _konfliktDialog;
|
||||
_sync.onFavoritenAnzahl = (anzahl) {
|
||||
// NEU-1-Fix: mounted-Guard (Loop läuft evtl. ohne geöffneten Tab)
|
||||
if (mounted) setState(() => _favServerCount = anzahl);
|
||||
};
|
||||
_sync.onPlaylistenLaden = () async {
|
||||
await _ladePlaylists();
|
||||
return _serverPlaylists.length;
|
||||
};
|
||||
_sync.onNachSync = () async {
|
||||
await _ladeStatus();
|
||||
await _ladeServerDaten();
|
||||
_berechneNaechstenSync();
|
||||
};
|
||||
_sync.onSyncEnde = _syncEnde;
|
||||
// Verbindung stellt der CloudService beim App-Start her (MeloHome.initState).
|
||||
// Hier nur Settings laden + Serverdaten, sobald der Service verbunden ist.
|
||||
_ladeSettings();
|
||||
@@ -114,7 +137,6 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
@override
|
||||
void dispose() {
|
||||
widget.cloud.removeListener(_onCloudStatus);
|
||||
_syncTimer?.cancel();
|
||||
_syncAnimController.dispose();
|
||||
_renameTitleCtrl.dispose();
|
||||
_renameArtistCtrl.dispose();
|
||||
@@ -138,18 +160,23 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
final p = await SharedPreferences.getInstance();
|
||||
final letzter = p.getString('cloud_last_sync');
|
||||
final letzterTs = p.getString('cloud_last_sync_ts');
|
||||
// F3: cloud_interval (0 = Aus). Migration von cloud_auto (alt: bool).
|
||||
var intervall = p.getInt('cloud_interval');
|
||||
if (intervall == null) {
|
||||
intervall = (p.getBool('cloud_auto') ?? true) ? 6 : 0;
|
||||
await p.setInt('cloud_interval', intervall);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_autoSync = p.getBool('cloud_auto') ?? true;
|
||||
_syncIntervall = p.getInt('cloud_interval') ?? 6;
|
||||
_syncIntervall = intervall ?? 0;
|
||||
_letzterSync = letzter ??
|
||||
(letzterTs != null
|
||||
? _formatZeit(DateTime.tryParse(letzterTs))
|
||||
: 'Nie');
|
||||
_letzterSyncTs = letzterTs;
|
||||
});
|
||||
}
|
||||
_berechneNaechstenSync();
|
||||
_starteAutoSync();
|
||||
}
|
||||
|
||||
Future<void> _ladeServerDaten() async {
|
||||
@@ -162,14 +189,14 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
}
|
||||
|
||||
void _berechneNaechstenSync() {
|
||||
if (!_autoSync || _syncIntervall == 0) {
|
||||
if (_syncIntervall == 0) {
|
||||
_naechsterSync = null;
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final last = _letzterSync != 'Nie'
|
||||
? DateTime.tryParse(_letzterSync)
|
||||
: now;
|
||||
final last = _letzterSyncTs != null
|
||||
? DateTime.tryParse(_letzterSyncTs!)
|
||||
: null;
|
||||
if (last != null) {
|
||||
final next = last.add(Duration(hours: _syncIntervall));
|
||||
if (next.isBefore(now)) {
|
||||
@@ -177,6 +204,8 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
} else {
|
||||
_naechsterSync = _formatZeit(next);
|
||||
}
|
||||
} else {
|
||||
_naechsterSync = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,226 +214,51 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
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 (_syncProzessLaeuft) return;
|
||||
final erfolgreich = await _syncAlles(automatisch: true);
|
||||
if (!erfolgreich) return; // cloud_last_sync nur bei Erfolg schreiben
|
||||
/// Intervall-Auswahl persistieren + App-weiten Auto-Sync-Timer neu starten.
|
||||
Future<void> _intervallSetzen(int stunden) async {
|
||||
setState(() => _syncIntervall = stunden);
|
||||
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();
|
||||
}
|
||||
await p.setInt('cloud_interval', stunden);
|
||||
await p.setBool('cloud_auto', stunden > 0); // Kompatibilität (altes Flag)
|
||||
await SyncService.starteAutoSyncTimer();
|
||||
_berechneNaechstenSync();
|
||||
}
|
||||
|
||||
// ─── 🔄 Komplett-Sync ───
|
||||
|
||||
/// Komplett-Sync. [automatisch]=true (Auto-Sync-Timer): Konflikte werden
|
||||
/// NICHT interaktiv gelöst — die Server-Metadaten gewinnen still.
|
||||
/// Rückgabe: true bei Erfolg, false bei Fehler oder wenn bereits ein
|
||||
/// Sync läuft (_syncProzessLaeuft-Guard).
|
||||
/// Komplett-Sync (F3): delegiert an den zentralen [SyncService], der auch
|
||||
/// ohne geöffneten Cloud-Tab weiterläuft (persistente Notification,
|
||||
/// Abschluss-Benachrichtigung, ☁️-Chip-Puls, globaler Doppel-Sync-Guard).
|
||||
Future<bool> _syncAlles({bool automatisch = false}) async {
|
||||
if (_syncProzessLaeuft) return false;
|
||||
_syncProzessLaeuft = true;
|
||||
_konfliktBatch = null;
|
||||
if (SyncService.laeuftGlobal) return false;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_syncLaeuft = true;
|
||||
_syncPhase = 'Verbinde...';
|
||||
_syncFortschritt = 0;
|
||||
_syncedItems = 0;
|
||||
_syncGesamt = 0;
|
||||
});
|
||||
}
|
||||
_syncAnimController.repeat();
|
||||
return _sync.syncAlles(automatisch: automatisch);
|
||||
}
|
||||
|
||||
/// Immer am Loop-Ende (auch bei Fehler / nach „Im Hintergrund fortsetzen“):
|
||||
/// Animation stoppen, Sync-Ansicht schließen. Controller-Zugriffe abgesichert,
|
||||
/// falls der Screen während des Hintergrund-Syncs disposed wurde (MED-4).
|
||||
void _syncEnde() {
|
||||
try {
|
||||
// Phase 1: Songs synchronisieren
|
||||
_updateSync('Vergleiche Songs...', 0.1);
|
||||
final serverSongs = await widget.cloud.listSongs();
|
||||
final db = DbHelper();
|
||||
final dir =
|
||||
Directory('${(await getApplicationDocumentsDirectory()).path}/music');
|
||||
if (!await dir.exists()) await dir.create(recursive: true);
|
||||
|
||||
int downloaded = 0;
|
||||
int totalNew = 0;
|
||||
|
||||
// Zähle neue Songs
|
||||
for (final song in serverSongs) {
|
||||
final sid = song['id']?.toString() ?? '';
|
||||
if (sid.isEmpty) continue;
|
||||
final existing = await db.songNachCloudId(sid);
|
||||
if (existing == null) totalNew++;
|
||||
}
|
||||
|
||||
// Downloade neue Songs + löse Konflikte (Titel lokal ≠ Server)
|
||||
int processed = 0;
|
||||
for (final song in serverSongs) {
|
||||
final sid = song['id']?.toString() ?? '';
|
||||
if (sid.isEmpty) continue;
|
||||
final title = (song['title'] ?? 'unknown').toString();
|
||||
final existing = await db.songNachCloudId(sid);
|
||||
if (existing != null) {
|
||||
// Konfliktprüfung: Server-Titel ODER Server-Künstler weichen ab
|
||||
final serverTitel = title.trim();
|
||||
final lokalerTitel = existing.titel.trim();
|
||||
final serverKuenstler = (song['artist']?.toString() ?? '').trim();
|
||||
final lokalerKuenstler = existing.kuenstler.trim();
|
||||
final titelWeichtAb = serverTitel.isNotEmpty &&
|
||||
lokalerTitel.toLowerCase() != serverTitel.toLowerCase();
|
||||
final kuenstlerWeichtAb = serverKuenstler.isNotEmpty &&
|
||||
lokalerKuenstler.toLowerCase() != serverKuenstler.toLowerCase();
|
||||
if (titelWeichtAb || kuenstlerWeichtAb) {
|
||||
// Batch: einmal gewählt → für alle weiteren Konflikte anwenden
|
||||
String? wahl;
|
||||
final batch = _konfliktBatch;
|
||||
if (batch != null) {
|
||||
wahl = batch == 'ueberspringen' ? 'lokal' : batch;
|
||||
} else if (automatisch) {
|
||||
// Auto-Sync: keine Dialoge — Server-Metadaten gewinnen still
|
||||
wahl = 'server';
|
||||
} else {
|
||||
wahl = await _konfliktDialog(existing, song);
|
||||
}
|
||||
if (wahl == 'server') {
|
||||
await db.cloudMetadatenAktualisieren(
|
||||
existing.id!,
|
||||
title: serverTitel,
|
||||
artist: serverKuenstler,
|
||||
);
|
||||
_syncedItems++;
|
||||
downloaded++;
|
||||
} 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++;
|
||||
}
|
||||
}
|
||||
// 'lokal' → nichts tun (lokale Version behalten)
|
||||
}
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_updateSync('Download: $title...',
|
||||
0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1)));
|
||||
final safeTitle = sanitizeDateiname(title);
|
||||
final dest = '${dir.path}/$safeTitle';
|
||||
if (await widget.cloud.download(sid, dest)) {
|
||||
downloaded++;
|
||||
// In DB eintragen mit cloud_id
|
||||
// (vereinfacht: ID3-Reader würde Titel extrahieren)
|
||||
}
|
||||
processed++;
|
||||
_syncedItems++;
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 50)); // UI-Update erlauben
|
||||
}
|
||||
|
||||
// Phase 2: Favoriten bidirektional synchronisieren
|
||||
// Root-Cause-Fix (Sprint D): Vorher wurden die Server-Favoriten nur
|
||||
// zurückgespiegelt (Server → Server) — lokale ⭐-Toggles gingen
|
||||
// verloren. Jetzt: Vereinigung lokal ∪ Server, Push + lokales Markieren.
|
||||
_updateSync('Sync Favoriten…', 0.55);
|
||||
final serverFavs = await widget.cloud.getFavorites();
|
||||
final serverIds = serverFavs
|
||||
.map((f) => f['id']?.toString() ?? '')
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
final lokalIds = await _favoriten.favoritenCloudIds();
|
||||
final merged = favoritenMerge(lokal: lokalIds, server: serverIds);
|
||||
// Lokal → Server: lokale Toggles erreichen den Server, Server-Favoriten
|
||||
// bleiben erhalten (kein Datenverlust in beide Richtungen)
|
||||
await widget.cloud.syncFavorites(merged);
|
||||
// Server → Lokal: Server-Favoriten lokal als ⭐ markieren (wenn Song
|
||||
// lokal existiert); `server_favorites` spiegelt den Merge-Zustand
|
||||
for (final cid in serverIds) {
|
||||
await _favoriten.merkeCloudFavorit(cid);
|
||||
}
|
||||
await db.serverFavoritesSet(merged);
|
||||
setState(() => _favServerCount = merged.length);
|
||||
_syncedItems += merged.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 + merged.length;
|
||||
});
|
||||
_setzeStatus(
|
||||
'$downloaded Songs + ${merged.length} Favoriten synchronisiert',
|
||||
ok: true);
|
||||
// Sync-Historie fürs Dashboard festhalten
|
||||
await _syncHistorieEintragen(
|
||||
dateien: downloaded,
|
||||
favoriten: merged.length,
|
||||
playlists: _serverPlaylists.length,
|
||||
);
|
||||
await _ladeStatus();
|
||||
}
|
||||
MeloLogger().aktion('cloud_sync_all', {
|
||||
'downloaded': downloaded,
|
||||
'favorites': merged.length,
|
||||
'playlists': _serverPlaylists.length,
|
||||
_syncAnimController.stop();
|
||||
_syncAnimController.value = 0;
|
||||
} catch (_) {
|
||||
// Controller kann bereits disposed sein (Screen verlassen)
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_syncLaeuft = false;
|
||||
_syncPhase = '';
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
MeloLogger().fehler('cloud_sync_all', e);
|
||||
_setzeStatus('Sync-Fehler: $e', ok: false);
|
||||
return false;
|
||||
} finally {
|
||||
// Guard IMMER freigeben — auch bei Fehler oder wenn der Screen während
|
||||
// des Syncs verlassen wurde („Im Hintergrund fortsetzen“).
|
||||
_syncProzessLaeuft = false;
|
||||
try {
|
||||
_syncAnimController.stop();
|
||||
_syncAnimController.value = 0;
|
||||
} catch (_) {
|
||||
// Controller kann bereits disposed sein (Screen verlassen)
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_syncLaeuft = false;
|
||||
_syncPhase = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,13 +340,15 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
/// Dismissable (Tap außerhalb = Abbruch → lokale Version behalten, der
|
||||
/// Sync läuft weiter). Batch-Optionen: Checkbox „Für alle übernehmen“ +
|
||||
/// Button „Alle weiteren überspringen“.
|
||||
/// Rückgabe: 'lokal' | 'server' | 'beide' | 'ueberspringen' | null
|
||||
Future<String?> _konfliktDialog(Song lokal, Map serverSong) async {
|
||||
/// Rückgabe: [KonfliktErgebnis] ('lokal' | 'server' | 'beide' |
|
||||
/// 'ueberspringen') oder null bei Abbruch. Die Batch-Logik übernimmt der
|
||||
/// SyncService (MED-3) — der Dialog liefert nur wahl + fuerAlle.
|
||||
Future<KonfliktErgebnis?> _konfliktDialog(Song lokal, Map serverSong) async {
|
||||
final serverTitel = (serverSong['title'] ?? '?').toString();
|
||||
final serverKuenstler = (serverSong['artist'] ?? '?').toString();
|
||||
if (!mounted) return null;
|
||||
var fuerAlle = false;
|
||||
return showDialog<String>(
|
||||
final wahl = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
@@ -581,24 +437,18 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
style: TextStyle(color: MeloTheme.rot)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_konfliktBatch = 'ueberspringen';
|
||||
Navigator.pop(ctx, 'ueberspringen');
|
||||
},
|
||||
onPressed: () => Navigator.pop(ctx, 'ueberspringen'),
|
||||
child: const Text('Alle weiteren überspringen',
|
||||
style: TextStyle(color: MeloTheme.textSekundaer)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).then((wahl) {
|
||||
// Batch merken: gewählte Entscheidung auf alle restlichen Konflikte
|
||||
// dieses Sync-Laufs anwenden.
|
||||
if (wahl != null && fuerAlle && _konfliktBatch == null) {
|
||||
_konfliktBatch = wahl;
|
||||
}
|
||||
return wahl;
|
||||
});
|
||||
);
|
||||
if (wahl == null) return null;
|
||||
// 'ueberspringen' = alle weiteren überspringen → fuerAlle=true, damit der
|
||||
// SyncService die Batch-Entscheidung (keep local) übernimmt.
|
||||
return KonfliktErgebnis(wahl, fuerAlle: wahl == 'ueberspringen' || fuerAlle);
|
||||
}
|
||||
|
||||
void _updateSync(String phase, double progress) {
|
||||
@@ -613,6 +463,7 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
// ─── 📋 Playlists ───
|
||||
|
||||
Future<void> _ladePlaylists() async {
|
||||
if (!mounted) return; // NEU-1: Loop kann ohne geöffneten Tab laufen
|
||||
setState(() => _ladtPlaylists = true);
|
||||
try {
|
||||
final pls = await widget.cloud.getPlaylists();
|
||||
@@ -706,6 +557,7 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
// ─── ⭐ Favoriten ───
|
||||
|
||||
Future<void> _ladeFavorites() async {
|
||||
if (!mounted) return; // NEU-1: Loop kann ohne geöffneten Tab laufen
|
||||
setState(() => _ladtFavorites = true);
|
||||
try {
|
||||
final favs = await widget.cloud.getFavorites();
|
||||
@@ -829,7 +681,7 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
}
|
||||
|
||||
Future<void> _download() async {
|
||||
if (_syncProzessLaeuft) {
|
||||
if (SyncService.laeuftGlobal) {
|
||||
_setzeStatus('Sync läuft bereits im Hintergrund', ok: false);
|
||||
return;
|
||||
}
|
||||
@@ -1075,7 +927,9 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
if (_syncedItems > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$_syncedItems Elemente synchronisiert',
|
||||
_syncGesamt > 0
|
||||
? '$_syncedItems / $_syncGesamt synchronisiert'
|
||||
: '$_syncedItems Elemente synchronisiert',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFA5D6A7), fontSize: 12),
|
||||
),
|
||||
@@ -1084,9 +938,10 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Nur die UI-Persistenz ändern: Fortschritts-Ansicht schließen.
|
||||
// Der Sync-Loop läuft weiter und der echte Guard
|
||||
// (_syncProzessLaeuft) bleibt gesperrt, bis der Loop fertig
|
||||
// ist — ein erneuter „Jetzt Syncen“ startet KEINEN Parallel-Sync.
|
||||
// Der Sync-Loop läuft im SyncService weiter und der globale
|
||||
// Guard (SyncService.laeuftGlobal) bleibt gesperrt, bis der
|
||||
// Loop fertig ist — ein erneuter „Jetzt Syncen“ startet KEINEN
|
||||
// Parallel-Sync. Sichtbar bleibt er via Notification + Chip-Puls.
|
||||
setState(() => _syncLaeuft = false);
|
||||
},
|
||||
child: const Text('Im Hintergrund fortsetzen',
|
||||
@@ -1128,7 +983,9 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
child: _aktionsButton(
|
||||
icon: Icons.sync_rounded,
|
||||
label: 'Jetzt Syncen',
|
||||
beschreibung: _autoSync ? 'Sofort synchronisieren' : 'Manuell syncen',
|
||||
beschreibung: _syncIntervall > 0
|
||||
? 'Sofort synchronisieren'
|
||||
: 'Manuell syncen',
|
||||
onTap: _download,
|
||||
)),
|
||||
],
|
||||
@@ -1359,7 +1216,10 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/// Auto-Sync-Intervall-Auswahl (F3): Aus/1h/3h/6h/12h.
|
||||
/// 0 = Aus (nur manuell) — ersetzt den alten Manuell/Auto-Toggle.
|
||||
Widget _syncModusAuswahl() {
|
||||
const optionen = [0, 1, 3, 6, 12];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -1370,119 +1230,53 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
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)),
|
||||
],
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8, top: 4),
|
||||
child: Text('Auto-Sync-Intervall',
|
||||
style: TextStyle(
|
||||
color: MeloTheme.textSekundaer,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500)),
|
||||
),
|
||||
// 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,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: optionen.map((h) {
|
||||
final aktiv = _syncIntervall == h;
|
||||
final label = h == 0 ? 'Aus' : '${h}h';
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: h != 12 ? 4 : 0),
|
||||
child: GestureDetector(
|
||||
onTap: () => _intervallSetzen(h),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: aktiv
|
||||
? Colors.white
|
||||
: MeloTheme.textSekundaer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).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,
|
||||
|
||||
Reference in New Issue
Block a user