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:
+4
-1
@@ -60,11 +60,14 @@ void main() async {
|
|||||||
?.createNotificationChannel(downloadChannel);
|
?.createNotificationChannel(downloadChannel);
|
||||||
|
|
||||||
// Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss)
|
// Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss)
|
||||||
|
// v2.55.2: Importance HIGH + Visibility public für Sperrbildschirm + Shadow.
|
||||||
|
// createNotificationChannel überschreibt bestehende Channel-Settings —
|
||||||
|
// ein alter Kanal mit low-Importance wird beim nächsten App-Start korrigiert.
|
||||||
const syncChannel = AndroidNotificationChannel(
|
const syncChannel = AndroidNotificationChannel(
|
||||||
'de.baka.melo.sync',
|
'de.baka.melo.sync',
|
||||||
'Melo Sync',
|
'Melo Sync',
|
||||||
description: 'Cloud-Sync-Fortschritt und Abschluss',
|
description: 'Cloud-Sync-Fortschritt und Abschluss',
|
||||||
importance: Importance.low,
|
importance: Importance.high,
|
||||||
playSound: false,
|
playSound: false,
|
||||||
enableVibration: false,
|
enableVibration: false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class _CloudScreenState extends State<CloudScreen>
|
|||||||
int _syncedItems = 0;
|
int _syncedItems = 0;
|
||||||
int _syncGesamt = 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,
|
/// Zentraler Sync-Loop (F3: läuft auch ohne geöffneten Tab weiter,
|
||||||
/// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls).
|
/// persistente Notification + Abschluss-Benachrichtigung + Chip-Puls).
|
||||||
late final SyncService _sync;
|
late final SyncService _sync;
|
||||||
@@ -230,6 +234,10 @@ class _CloudScreenState extends State<CloudScreen>
|
|||||||
/// Komplett-Sync (F3): delegiert an den zentralen [SyncService], der auch
|
/// Komplett-Sync (F3): delegiert an den zentralen [SyncService], der auch
|
||||||
/// ohne geöffneten Cloud-Tab weiterläuft (persistente Notification,
|
/// ohne geöffneten Cloud-Tab weiterläuft (persistente Notification,
|
||||||
/// Abschluss-Benachrichtigung, ☁️-Chip-Puls, globaler Doppel-Sync-Guard).
|
/// 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 {
|
Future<bool> _syncAlles({bool automatisch = false}) async {
|
||||||
if (SyncService.laeuftGlobal) return false;
|
if (SyncService.laeuftGlobal) return false;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -242,12 +250,134 @@ class _CloudScreenState extends State<CloudScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_syncAnimController.repeat();
|
_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);
|
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“):
|
/// Immer am Loop-Ende (auch bei Fehler / nach „Im Hintergrund fortsetzen“):
|
||||||
/// Animation stoppen, Sync-Ansicht schließen. Controller-Zugriffe abgesichert,
|
/// Animation stoppen, Sync-Ansicht schließen. Controller-Zugriffe abgesichert,
|
||||||
/// falls der Screen während des Hintergrund-Syncs disposed wurde (MED-4).
|
/// 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() {
|
void _syncEnde() {
|
||||||
try {
|
try {
|
||||||
_syncAnimController.stop();
|
_syncAnimController.stop();
|
||||||
@@ -255,11 +385,41 @@ class _CloudScreenState extends State<CloudScreen>
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Controller kann bereits disposed sein (Screen verlassen)
|
// 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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_syncLaeuft = false;
|
_syncLaeuft = false;
|
||||||
_syncPhase = '';
|
_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: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../utils/sanitize.dart';
|
import '../utils/sanitize.dart';
|
||||||
@@ -253,11 +254,19 @@ class SyncService {
|
|||||||
/// NICHT interaktiv gelöst — die Server-Metadaten gewinnen still.
|
/// NICHT interaktiv gelöst — die Server-Metadaten gewinnen still.
|
||||||
/// Rückgabe: true bei Erfolg, false bei Fehler oder wenn bereits ein
|
/// Rückgabe: true bei Erfolg, false bei Fehler oder wenn bereits ein
|
||||||
/// Sync läuft (globaler Guard).
|
/// Sync läuft (globaler Guard).
|
||||||
|
///
|
||||||
|
/// v2.55.2: Startet den Foreground-Service (FlutterBackgroundService),
|
||||||
|
/// damit der Sync auch bei Display-aus und minimierter App weiterläuft.
|
||||||
Future<bool> syncAlles({bool automatisch = false}) async {
|
Future<bool> syncAlles({bool automatisch = false}) async {
|
||||||
if (_laeuftGlobal) return false;
|
if (_laeuftGlobal) return false;
|
||||||
_laeuftGlobal = true;
|
_laeuftGlobal = true;
|
||||||
laeuftNotifier.value = true;
|
laeuftNotifier.value = true;
|
||||||
String? konfliktBatch;
|
String? konfliktBatch;
|
||||||
|
|
||||||
|
// v2.55.2: Foreground-Service starten — hält den Prozess im Hintergrund
|
||||||
|
// aktiv, auch wenn das Display aus ist oder die App minimiert wird.
|
||||||
|
await _starteForegroundService();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_zeigeSyncNotification('Verbinde…', 0, 0);
|
_zeigeSyncNotification('Verbinde…', 0, 0);
|
||||||
onFortschritt?.call('Verbinde...', 0);
|
onFortschritt?.call('Verbinde...', 0);
|
||||||
@@ -527,11 +536,41 @@ class SyncService {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Plugin kann beim App-Exit bereits disposed sein
|
// Plugin kann beim App-Exit bereits disposed sein
|
||||||
}
|
}
|
||||||
|
// v2.55.2: Foreground-Service stoppen, sobald der Sync beendet ist.
|
||||||
|
await _stoppeForegroundService();
|
||||||
onSyncEnde?.call();
|
onSyncEnde?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Notifications ───
|
// ─── Notifications (v2.55.2: HIGH + public für Sperrbildschirm) ───
|
||||||
|
|
||||||
|
/// Erzeugt die [AndroidNotificationDetails] für Sync-Notifications.
|
||||||
|
/// Statisch → testbar ohne Platform-/Plugin-Zugriff.
|
||||||
|
/// [ongoing]=true für Fortschritts-Notification (nicht wegwischbar),
|
||||||
|
/// [ongoing]=false für Abschluss-/Fehler-Notification (automatisch gelöscht).
|
||||||
|
@visibleForTesting
|
||||||
|
static AndroidNotificationDetails syncNotificationDetails({
|
||||||
|
required bool ongoing,
|
||||||
|
int progress = 0,
|
||||||
|
int maxProgress = 0,
|
||||||
|
}) {
|
||||||
|
return AndroidNotificationDetails(
|
||||||
|
_syncChannelId,
|
||||||
|
'Melo Sync',
|
||||||
|
channelDescription: ongoing
|
||||||
|
? 'Cloud-Sync-Fortschritt'
|
||||||
|
: 'Cloud-Sync-Abschluss',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
visibility: NotificationVisibility.public,
|
||||||
|
onlyAlertOnce: true,
|
||||||
|
showProgress: ongoing,
|
||||||
|
maxProgress: ongoing ? (maxProgress > 0 ? maxProgress : 1) : 0,
|
||||||
|
progress: ongoing ? progress : 0,
|
||||||
|
ongoing: ongoing,
|
||||||
|
autoCancel: !ongoing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Persistente Fortschritts-Notification (nicht wegwischbar, ongoing).
|
/// Persistente Fortschritts-Notification (nicht wegwischbar, ongoing).
|
||||||
void _zeigeSyncNotification(String body, int aktuell, int gesamt) {
|
void _zeigeSyncNotification(String body, int aktuell, int gesamt) {
|
||||||
@@ -543,18 +582,10 @@ class SyncService {
|
|||||||
title: 'Synchronisiere…',
|
title: 'Synchronisiere…',
|
||||||
body: gesamt > 0 ? '$aktuell / $gesamt Songs' : body,
|
body: gesamt > 0 ? '$aktuell / $gesamt Songs' : body,
|
||||||
notificationDetails: NotificationDetails(
|
notificationDetails: NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: syncNotificationDetails(
|
||||||
_syncChannelId,
|
|
||||||
'Melo Sync',
|
|
||||||
channelDescription: 'Cloud-Sync-Fortschritt',
|
|
||||||
importance: Importance.low,
|
|
||||||
priority: Priority.low,
|
|
||||||
onlyAlertOnce: true,
|
|
||||||
showProgress: true,
|
|
||||||
maxProgress: maxP,
|
|
||||||
progress: p,
|
|
||||||
ongoing: true,
|
ongoing: true,
|
||||||
autoCancel: false,
|
progress: p,
|
||||||
|
maxProgress: maxP,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -573,14 +604,7 @@ class SyncService {
|
|||||||
body: '$songs Songs, $favoriten Favoriten synchronisiert ✅',
|
body: '$songs Songs, $favoriten Favoriten synchronisiert ✅',
|
||||||
payload: 'sync_fertig',
|
payload: 'sync_fertig',
|
||||||
notificationDetails: NotificationDetails(
|
notificationDetails: NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: syncNotificationDetails(ongoing: false),
|
||||||
_syncChannelId,
|
|
||||||
'Melo Sync',
|
|
||||||
channelDescription: 'Cloud-Sync-Abschluss',
|
|
||||||
importance: Importance.defaultImportance,
|
|
||||||
priority: Priority.defaultPriority,
|
|
||||||
autoCancel: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -597,18 +621,44 @@ class SyncService {
|
|||||||
body: 'Bitte erneut versuchen ❌',
|
body: 'Bitte erneut versuchen ❌',
|
||||||
payload: 'sync_fehler',
|
payload: 'sync_fehler',
|
||||||
notificationDetails: NotificationDetails(
|
notificationDetails: NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: syncNotificationDetails(ongoing: false),
|
||||||
_syncChannelId,
|
|
||||||
'Melo Sync',
|
|
||||||
channelDescription: 'Cloud-Sync-Abschluss',
|
|
||||||
importance: Importance.defaultImportance,
|
|
||||||
priority: Priority.defaultPriority,
|
|
||||||
autoCancel: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Foreground-Service (v2.55.2) ───
|
||||||
|
|
||||||
|
/// Startet den Foreground-Service, damit der Sync auch bei Display-aus
|
||||||
|
/// und minimierter App weiterläuft (Android-Prozess-Garantie).
|
||||||
|
Future<void> _starteForegroundService() async {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
try {
|
||||||
|
final svc = FlutterBackgroundService();
|
||||||
|
final laeuft = await svc.isRunning();
|
||||||
|
if (!laeuft) {
|
||||||
|
await svc.startService();
|
||||||
|
MeloLogger().aktion('foreground_sync_start', {});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
MeloLogger().fehler('foreground_sync_start', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stoppt den Foreground-Service nach Sync-Ende.
|
||||||
|
Future<void> _stoppeForegroundService() async {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
try {
|
||||||
|
final svc = FlutterBackgroundService();
|
||||||
|
final laeuft = await svc.isRunning();
|
||||||
|
if (laeuft) {
|
||||||
|
svc.invoke('stopService'); // Feuer-und-vergessen (Isolate-Nachricht)
|
||||||
|
MeloLogger().aktion('foreground_sync_stop', {});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
MeloLogger().fehler('foreground_sync_stop', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _formatZeit(DateTime? dt) {
|
String _formatZeit(DateTime? dt) {
|
||||||
if (dt == null) return 'Nie';
|
if (dt == null) return 'Nie';
|
||||||
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:melo_app/services/sync_service.dart';
|
import 'package:melo_app/services/sync_service.dart';
|
||||||
|
import 'package:melo_app/services/realtime_sync_service.dart';
|
||||||
|
|
||||||
/// SyncService-Tests (Sprint E, MED-3-Review-Fix).
|
/// SyncService-Tests (Sprint E, MED-3-Review-Fix, v2.55.2).
|
||||||
///
|
///
|
||||||
/// Abgedeckt:
|
/// Abgedeckt:
|
||||||
/// - `tombstoneAnwenden`-Matrix (null-letzterSync / null-deletedAt /
|
/// - `tombstoneAnwenden`-Matrix (null-letzterSync / null-deletedAt /
|
||||||
@@ -9,8 +11,10 @@ import 'package:melo_app/services/sync_service.dart';
|
|||||||
/// - `SyncBericht` (hatAenderungen, zusammenfassung, details)
|
/// - `SyncBericht` (hatAenderungen, zusammenfassung, details)
|
||||||
/// - `songAusServerMap` (MED-2: Server-Listeneintrag → lokaler Song-DB-
|
/// - `songAusServerMap` (MED-2: Server-Listeneintrag → lokaler Song-DB-
|
||||||
/// Eintrag mit cloud_id + Titeldaten)
|
/// Eintrag mit cloud_id + Titeldaten)
|
||||||
///
|
/// - v2.55.2: `syncNotificationDetails` (Importance.high,
|
||||||
/// Reine, statische Funktionen — kein DB-/Plattform-Zugriff nötig.
|
/// Priority.high, Visibility.public für Sperrbildschirm)
|
||||||
|
/// - v2.55.2: Sync-Service von Lifecycle unabhängig
|
||||||
|
/// (RealtimeSyncService.pausiere() ≠ SyncService stoppen)
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -174,4 +178,78 @@ void main() {
|
|||||||
expect(song.groesseBytes, 1024);
|
expect(song.groesseBytes, 1024);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── v2.55.2: Notification-Konfiguration ───
|
||||||
|
|
||||||
|
group('syncNotificationDetails (v2.55.2: HIGH + public)', () {
|
||||||
|
test('ongoing=true → Importance.high, Priority.high, showProgress', () {
|
||||||
|
final details =
|
||||||
|
SyncService.syncNotificationDetails(ongoing: true, progress: 5, maxProgress: 25);
|
||||||
|
|
||||||
|
expect(details.importance, Importance.high);
|
||||||
|
expect(details.priority, Priority.high);
|
||||||
|
expect(details.visibility, NotificationVisibility.public);
|
||||||
|
expect(details.showProgress, isTrue);
|
||||||
|
expect(details.ongoing, isTrue);
|
||||||
|
expect(details.autoCancel, isFalse);
|
||||||
|
expect(details.progress, 5);
|
||||||
|
expect(details.maxProgress, 25);
|
||||||
|
expect(details.onlyAlertOnce, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ongoing=false → autoCancel, kein Progress', () {
|
||||||
|
final details = SyncService.syncNotificationDetails(ongoing: false);
|
||||||
|
|
||||||
|
expect(details.importance, Importance.high);
|
||||||
|
expect(details.priority, Priority.high);
|
||||||
|
expect(details.visibility, NotificationVisibility.public);
|
||||||
|
expect(details.showProgress, isFalse);
|
||||||
|
expect(details.ongoing, isFalse);
|
||||||
|
expect(details.autoCancel, isTrue);
|
||||||
|
expect(details.progress, 0);
|
||||||
|
expect(details.maxProgress, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maxProgress=0 bei ongoing=true → default 1 (Division durch 0)', () {
|
||||||
|
final details =
|
||||||
|
SyncService.syncNotificationDetails(ongoing: true, progress: 0, maxProgress: 0);
|
||||||
|
|
||||||
|
// maxProgress muss > 0 sein, sonst crasht die Notification-API
|
||||||
|
expect(details.maxProgress, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('channelId ist de.baka.melo.sync', () {
|
||||||
|
final details = SyncService.syncNotificationDetails(ongoing: false);
|
||||||
|
expect(details.channelId, 'de.baka.melo.sync');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── v2.55.2: Lifecycle-Unabhängigkeit ───
|
||||||
|
|
||||||
|
group('SyncService unabhängig von RealtimeSyncService-Lifecycle', () {
|
||||||
|
test('laeuftGlobal wird NICHT durch pausiere() zurückgesetzt', () {
|
||||||
|
// SyncService.laeuftGlobal startet als false.
|
||||||
|
// Der Lifecycle (RealtimeSyncService.pausiere) darf diesen Zustand
|
||||||
|
// NICHT beeinflussen — es sind getrennte Systeme.
|
||||||
|
expect(SyncService.laeuftGlobal, isFalse);
|
||||||
|
|
||||||
|
// Lifecycle: App geht in den Hintergrund
|
||||||
|
RealtimeSyncService().pausiere();
|
||||||
|
|
||||||
|
// SyncService muss unabhängig bleiben
|
||||||
|
expect(SyncService.laeuftGlobal, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('laeuftNotifier wird NICHT durch pausiere() verändert', () {
|
||||||
|
final vorher = SyncService.laeuftNotifier.value;
|
||||||
|
RealtimeSyncService().pausiere();
|
||||||
|
expect(SyncService.laeuftNotifier.value, vorher);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('syncBenachrichtigungGetippt wird NICHT durch pausiere() verändert', () {
|
||||||
|
final vorher = SyncService.syncBenachrichtigungGetippt.value;
|
||||||
|
RealtimeSyncService().pausiere();
|
||||||
|
expect(SyncService.syncBenachrichtigungGetippt.value, vorher);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user