v2.55.2 — Manueller Sync: Status-Popup, Hintergrund-Weiterlauf, Sperrbildschirm-Notification

- cloud_screen.dart: Sync-Popup mit Live-Fortschritt („12/325 synchronisiert"),
  „Im Hintergrund fortsetzen"-Button, Ergebnis-Snackbar nach Abschluss
- sync_service.dart: Notification-Kanal auf HIGH + Visibility.public,
  Foreground-Service starten/stoppen während Sync (Display-aus-Garantie),
  statische syncNotificationDetails() für Tests
- main.dart: Sync-Channel-Importance auf HIGH (überschreibt alte low-Einstellung)
- Tests: +7 (syncNotificationDetails HIGH/public, Lifecycle-Unabhängigkeit)
This commit is contained in:
Dustin
2026-08-06 18:11:41 +02:00
parent b4d63509d1
commit 7777297073
4 changed files with 323 additions and 32 deletions
+160
View File
@@ -56,6 +56,10 @@ class _CloudScreenState extends State<CloudScreen>
int _syncedItems = 0;
int _syncGesamt = 0;
/// v2.55.2: BuildContext des aktiven Sync-Popups, damit der Dialog
/// aus _syncEnde() heraus geschlossen werden kann.
BuildContext? _syncPopupContext;
/// Zentraler Sync-Loop (F3: läuft auch ohne geöffneten Tab weiter,
/// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls).
late final SyncService _sync;
@@ -230,6 +234,10 @@ class _CloudScreenState extends State<CloudScreen>
/// 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).
///
/// v2.55.2: Zeigt ein Popup mit Live-Fortschritt („12/325 synchronisiert“),
/// das per [Im Hintergrund fortsetzen] geschlossen werden kann — der Sync
/// läuft dann im Hintergrund weiter (Foreground-Service + Notification).
Future<bool> _syncAlles({bool automatisch = false}) async {
if (SyncService.laeuftGlobal) return false;
if (mounted) {
@@ -242,12 +250,134 @@ class _CloudScreenState extends State<CloudScreen>
});
}
_syncAnimController.repeat();
// v2.55.2: Popup mit Live-Fortschritt anzeigen (nur bei manuellem Sync).
// Der Dialog läuft mittels StatefulBuilder und schließt sich automatisch,
// wenn der Sync beendet ist, oder per „Im Hintergrund fortsetzen“-
// Button (dann läuft der Sync im Service weiter).
if (!automatisch && mounted) {
_zeigeSyncPopup();
}
return _sync.syncAlles(automatisch: automatisch);
}
/// v2.55.2: Popup-Dialog mit Live-Sync-Fortschritt.
/// Läuft parallel zum SyncService-Loop — die Fortschritts-Callbacks
/// (_syncedItems/_syncGesamt) werden via setState im Dialog aktualisiert.
/// Der Dialog hat zwei Ausgänge:
/// 1. Sync beendet → Dialog schließt automatisch + Ergebnis anzeigen
/// 2. „Im Hintergrund fortsetzen“ → Dialog schließt, Sync läuft weiter
void _zeigeSyncPopup() {
_syncPopupContext = null;
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) {
_syncPopupContext = ctx;
return StatefulBuilder(
builder: (ctx, setDialogState) {
return AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: Row(
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: MeloTheme.rot,
strokeWidth: 2,
),
),
const SizedBox(width: 12),
const Text('Sync läuft',
style: TextStyle(color: Colors.white, fontSize: 17)),
],
),
content: SizedBox(
width: 280,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fortschrittsbalken
if (_syncGesamt > 0) ...[
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: _syncGesamt > 0
? (_syncedItems / _syncGesamt).clamp(0.0, 1.0)
: null,
backgroundColor: MeloTheme.dunkel2,
color: MeloTheme.rot,
minHeight: 8,
),
),
const SizedBox(height: 12),
],
// Fortschritts-Text (z.B. „12/325 synchronisiert“)
Text(
_syncGesamt > 0
? '$_syncedItems / $_syncGesamt synchronisiert'
: _syncPhase.isNotEmpty
? _syncPhase
: 'Synchronisiere…',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600),
),
if (_syncPhase.isNotEmpty && _syncGesamt > 0) ...[
const SizedBox(height: 4),
Text(
_syncPhase,
style: const TextStyle(
color: MeloTheme.textSekundaer, fontSize: 12),
),
],
const SizedBox(height: 16),
// Button: Im Hintergrund fortsetzen
SizedBox(
width: double.infinity,
child: TextButton.icon(
onPressed: () {
_syncPopupContext = null;
Navigator.pop(ctx);
},
icon: const Icon(Icons.phone_android,
color: MeloTheme.textSekundaer, size: 18),
label: const Text('Im Hintergrund fortsetzen',
style: TextStyle(
color: MeloTheme.textSekundaer,
fontSize: 13)),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: MeloTheme.dunkel2, width: 1),
),
),
),
),
],
),
),
);
},
);
},
).then((_) {
_syncPopupContext = null;
});
}
/// 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).
///
/// v2.55.2: Schließt das Sync-Popup (falls noch offen) und zeigt das
/// Ergebnis als Snackbar an.
void _syncEnde() {
try {
_syncAnimController.stop();
@@ -255,11 +385,41 @@ class _CloudScreenState extends State<CloudScreen>
} catch (_) {
// Controller kann bereits disposed sein (Screen verlassen)
}
// v2.55.2: Sync-Popup schließen (falls noch offen)
final popupCtx = _syncPopupContext;
if (popupCtx != null && mounted) {
try {
Navigator.of(popupCtx).pop();
} catch (_) {
// Dialog wurde bereits geschlossen (z.B. „Im Hintergrund fortsetzen“)
}
_syncPopupContext = null;
}
if (mounted) {
setState(() {
_syncLaeuft = false;
_syncPhase = '';
});
// v2.55.2: Ergebnis-Snackbar anzeigen (nur wenn Screen noch sichtbar)
if (_status != null && _status!.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_status!,
style: const TextStyle(color: Colors.white, fontSize: 14)),
backgroundColor: _statusOk ? MeloTheme.dunkel2 : const Color(0xFF3B0D0D),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'OK',
textColor: MeloTheme.rot,
onPressed: () {},
),
),
);
}
}
}