v2.52 — Favoriten-Sync-Fix: bidirektionaler Merge (lokal↔Server), Toggle-Push sofort, cloud_id-Verdrahtung

This commit is contained in:
Dustin
2026-08-05 10:33:23 +02:00
parent 4694955f5f
commit 3b898e238e
7 changed files with 286 additions and 17 deletions
+12
View File
@@ -262,6 +262,18 @@ class CloudService extends ChangeNotifier {
return await _post('/api/v1/cloud/favorites/toggle', {'song_id': songId});
}
/// Favoriten-Status deterministisch setzen (KEIN Toggle): der Server
/// erzwingt den gewünschten Zustand (`set`-Parameter). Verhindert
/// Doppel-Toggle, wenn der Server-Zustand vom lokalen abweicht
/// (Feuer-und-Vergessen-Push beim lokalen Favoriten-Toggle).
Future<bool> setFavorite(String songId, {required bool favorit}) async {
final r = await _post('/api/v1/cloud/favorites/toggle', {
'song_id': songId,
'set': favorit,
});
return r?['status'] == 'ok';
}
// ─── ✏️ Umbenennen ───
/// Song umbenennen (benutzerdefinierter Titel/Artist)
+47 -2
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../database/db_helper.dart';
import '../models/song.dart';
@@ -11,6 +12,11 @@ class FavoritenService {
final DbHelper _db = DbHelper();
int? _favoritenPlaylistId;
/// Wird nach jedem erfolgreichen lokalen Toggle aufgerufen (Feuer-und-
/// Vergessen → Server-Push). Wird in `main()` verdrahtet; Fehler werden
/// beim nächsten vollständigen Sync korrigiert (favoritenMerge-Vereinigung).
Future<void> Function(int songId, bool neuerStatus)? onStatusGeaendert;
int? get favoritenId => _favoritenPlaylistId;
Future<void> init() async {
@@ -32,18 +38,32 @@ class FavoritenService {
/// Schaltet den Favoriten-Status um und gibt den NEUEN Status zurück
/// (true = jetzt Favorit). Bei Fehlern (z. B. DB-Race) wird der Zustand
/// aus der DB neu geladen statt blind geflippt — kein unhandled Throw.
/// Nach dem lokalen Toggle wird [onStatusGeaendert] Feuer-und-Vergessen
/// aufgerufen (Server-Push; Retry = nächster vollständiger Sync).
Future<bool> umschalten(int songId) async {
if (_favoritenPlaylistId == null) return false;
try {
bool neuerStatus;
if (await istFavorit(songId)) {
await _db.songAusPlaylistEntfernen(_favoritenPlaylistId!, songId);
return false;
neuerStatus = false;
} else {
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
// INSERT OR IGNORE in songZurPlaylist fängt Doppel-Tap-Races ab
await _db.songZurPlaylist(_favoritenPlaylistId!, songId, songs.length);
return true;
neuerStatus = true;
}
final cb = onStatusGeaendert;
if (cb != null) {
unawaited(() async {
try {
await cb(songId, neuerStatus);
} catch (e) {
MeloLogger().fehler('favoriten_server_push', e);
}
}());
}
return neuerStatus;
} catch (e) {
debugPrint('Favoriten-Toggle fehlgeschlagen: $e');
MeloLogger().fehler('favoriten_toggle', e);
@@ -57,6 +77,31 @@ class FavoritenService {
return _db.songsDerPlaylist(_favoritenPlaylistId!);
}
/// Cloud-IDs aller lokalen ⭐-Favoriten (Push-Richtung lokal → Server).
/// Songs ohne cloud_id (nicht hochgeladen) werden übersprungen.
Future<Set<String>> favoritenCloudIds() async {
if (_favoritenPlaylistId == null) return {};
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
return songs
.map((s) => s.cloudId)
.whereType<String>()
.where((c) => c.isNotEmpty)
.toSet();
}
/// Markiert einen Server-Favoriten (cloud_id) lokal als ⭐-Favorit
/// (Richtung Server → lokal). Fehlt der Song lokal, passiert nichts —
/// der Favorit bleibt in `server_favorites` fürs Dashboard erhalten.
Future<void> merkeCloudFavorit(String cloudId) async {
if (_favoritenPlaylistId == null) return;
final song = await _db.songNachCloudId(cloudId);
final id = song?.id;
if (id == null) return;
if (await istFavorit(id)) return;
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
await _db.songZurPlaylist(_favoritenPlaylistId!, id, songs.length);
}
Future<int> anzahlFavoriten() async {
if (_favoritenPlaylistId == null) await init();
if (_favoritenPlaylistId == null) return 0;
+39
View File
@@ -0,0 +1,39 @@
import 'cloud_service.dart';
/// Bidirektionaler Favoriten-Sync (Sprint D, v2.52).
///
/// Root-Cause des alten Bugs: `_syncAlles` las die Server-Favoriten und
/// schrieb sie per `syncFavorites` UNVERÄNDERT zurück auf den Server —
/// lokale ⭐-Toggles gingen verloren, `server_favorites` wurde nie genutzt.
///
/// Neues Verhalten (Merge statt Server-Spiegel):
/// 1. **Lokal → Server:** Alle cloud_ids lokaler ⭐-Favoriten werden
/// zusammen mit den Server-Favoriten hochgeladen (Vereinigung). Lokale
/// Toggles erreichen den Server damit garantiert.
/// 2. **Server → Lokal:** Server-Favoriten werden lokal als ⭐ markiert,
/// sobald der Song lokal existiert (cloud_id-Verknüpfung).
/// 3. **Feuer-und-Vergessen beim Toggle:** `CloudService.setFavorite`
/// (deterministisch, kein Toggle) direkt beim lokalen Umschalten; ein
/// Fehlschlag wird durch den nächsten vollständigen Sync korrigiert.
/// Vereinigt lokale und Server-Favoriten (cloud_ids) — keine Datenverluste
/// in beide Richtungen. Sortiert für deterministische Tests.
List<String> favoritenMerge({
required Set<String> lokal,
required Set<String> server,
}) {
final merged = <String>{...lokal, ...server}.toList()..sort();
return merged;
}
/// Feuer-und-Vergessen-Push eines Favoriten-Toggles an den Server.
/// Deterministisch via `set` (kein Doppel-Toggle-Risiko). Fehler werden
/// still geschluckt — der nächste vollständige Sync korrigiert den Zustand
/// (favoritenMerge-Vereinigung).
Future<void> pusheFavoritAnServer(
CloudService cloud,
String cloudId,
bool favorit,
) async {
await cloud.setFavorite(cloudId, favorit: favorit);
}