import 'dart:async'; import 'package:flutter/foundation.dart'; import '../database/db_helper.dart'; import '../models/song.dart'; import 'melo_logger.dart'; class FavoritenService { static final FavoritenService _instanz = FavoritenService._(); factory FavoritenService() => _instanz; 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 Function(int songId, bool neuerStatus)? onStatusGeaendert; int? get favoritenId => _favoritenPlaylistId; Future init() async { final playlists = await _db.allePlaylists(); final vorhanden = playlists.where((p) => p['name'] == '⭐ Favoriten').toList(); if (vorhanden.isNotEmpty) { _favoritenPlaylistId = vorhanden.first['id'] as int; } else { _favoritenPlaylistId = await _db.playlistErstellen('⭐ Favoriten'); } } Future istFavorit(int songId) async { if (_favoritenPlaylistId == null) return false; final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!); return songs.any((s) => s.id == songId); } /// 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 umschalten(int songId) async { if (_favoritenPlaylistId == null) return false; try { bool neuerStatus; if (await istFavorit(songId)) { await _db.songAusPlaylistEntfernen(_favoritenPlaylistId!, songId); 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); 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); // Zustand nach Fehler aus der DB synchronisieren (Quelle der Wahrheit) return istFavorit(songId); } } Future> alleFavoriten() async { if (_favoritenPlaylistId == null) return []; return _db.songsDerPlaylist(_favoritenPlaylistId!); } /// Cloud-IDs aller lokalen ⭐-Favoriten (Push-Richtung lokal → Server). /// Songs ohne cloud_id (nicht hochgeladen) werden übersprungen. Future> favoritenCloudIds() async { if (_favoritenPlaylistId == null) return {}; final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!); return songs .map((s) => s.cloudId) .whereType() .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 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 anzahlFavoriten() async { if (_favoritenPlaylistId == null) await init(); if (_favoritenPlaylistId == null) return 0; final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!); return songs.length; } Future> favoritenIds() async { if (_favoritenPlaylistId == null) return {}; final d = await _db.db; final rows = await d.rawQuery( 'SELECT song_id FROM playlist_songs WHERE playlist_id = ?', [_favoritenPlaylistId], ); return rows.map((r) => r['song_id'] as int).toSet(); } }