## Fix (Code-Review Finding 2, HIGH) - DbHelper.songZurPlaylist: INSERT OR IGNORE (ConflictAlgorithm.ignore) — Doppel-Tap-Race verletzt PRIMARY KEY (playlist_id, song_id) nicht mehr - FavoritenService.umschalten: gibt neuen Status zurück, try/catch mit MeloLogger, Zustand nach Fehler aus DB neu geladen statt blind geflippt - NowPlayingScreen._toggleFavorit: In-Flight-Guard _toggleLaeuft gegen parallele Toggles + Statusübernahme aus DB (Quelle der Wahrheit)
77 lines
2.6 KiB
Dart
77 lines
2.6 KiB
Dart
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;
|
|
|
|
int? get favoritenId => _favoritenPlaylistId;
|
|
|
|
Future<void> 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<bool> 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.
|
|
Future<bool> umschalten(int songId) async {
|
|
if (_favoritenPlaylistId == null) return false;
|
|
try {
|
|
if (await istFavorit(songId)) {
|
|
await _db.songAusPlaylistEntfernen(_favoritenPlaylistId!, songId);
|
|
return 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;
|
|
}
|
|
} 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<List<Song>> alleFavoriten() async {
|
|
if (_favoritenPlaylistId == null) return [];
|
|
return _db.songsDerPlaylist(_favoritenPlaylistId!);
|
|
}
|
|
|
|
Future<int> anzahlFavoriten() async {
|
|
if (_favoritenPlaylistId == null) await init();
|
|
if (_favoritenPlaylistId == null) return 0;
|
|
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
|
|
return songs.length;
|
|
}
|
|
|
|
Future<Set<int>> 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();
|
|
}
|
|
}
|