From bd692d4682a133b5ef1c13a4370443049715d7ae Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 4 Aug 2026 16:46:49 +0200 Subject: [PATCH] =?UTF-8?q?v2.49.4=20=E2=80=94=20Favoriten-Toggle=20race-s?= =?UTF-8?q?icher=20(kein=20PK-Crash=20bei=20Doppel-Tap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- lib/database/db_helper.dart | 4 +++- lib/screens/now_playing_screen.dart | 18 ++++++++++++++++-- lib/services/favoriten_service.dart | 29 ++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/database/db_helper.dart b/lib/database/db_helper.dart index 083d3fe..84ac286 100644 --- a/lib/database/db_helper.dart +++ b/lib/database/db_helper.dart @@ -401,11 +401,13 @@ class DbHelper { Future songZurPlaylist(int playlistId, int songId, int position) async { final d = await db; + // INSERT OR IGNORE: Song bereits in der Playlist (Race/Doppel-Tap) ist + // kein Fehler — der Eintrag bleibt einfach bestehen (PK-Konflikt abgefangen). await d.insert('playlist_songs', { 'playlist_id': playlistId, 'song_id': songId, 'position': position, - }); + }, conflictAlgorithm: ConflictAlgorithm.ignore); } Future>> allePlaylists() async { diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 6d833d6..53365dc 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -6,6 +6,7 @@ import 'package:just_audio/just_audio.dart'; import '../models/song.dart'; import '../services/favoriten_service.dart'; import '../services/lyrics_service.dart'; +import '../services/melo_logger.dart'; import '../services/player_service.dart'; import '../utils/farb_theme.dart'; import '../widgets/warteschlange_sheet.dart'; @@ -48,6 +49,7 @@ class _NowPlayingScreenState extends State { // ─── Favoriten (Like) ─── bool _istFavorit = false; + bool _toggleLaeuft = false; // In-Flight-Guard gegen Doppel-Tap-Race @override void initState() { @@ -136,12 +138,24 @@ class _NowPlayingScreenState extends State { } Future _toggleFavorit() async { + if (_toggleLaeuft) return; // zweiter Tap während laufendem Toggle ignorieren final song = _player.aktuellerSong; final id = song?.id; if (song == null || id == null) return; - await _favoriten.umschalten(id); + _toggleLaeuft = true; + bool neuerStatus; + try { + neuerStatus = await _favoriten.umschalten(id); + } catch (e) { + debugPrint('Favoriten-Toggle fehlgeschlagen: $e'); + MeloLogger().fehler('favoriten_toggle_ui', e); + neuerStatus = _istFavorit; // unverändert lassen + } finally { + _toggleLaeuft = false; + } if (!mounted) return; - setState(() => _istFavorit = !_istFavorit); + // Zustand aus der DB (Quelle der Wahrheit) übernehmen statt blind zu flippen + setState(() => _istFavorit = neuerStatus); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_istFavorit diff --git a/lib/services/favoriten_service.dart b/lib/services/favoriten_service.dart index cb07bb6..21bd805 100644 --- a/lib/services/favoriten_service.dart +++ b/lib/services/favoriten_service.dart @@ -1,5 +1,7 @@ +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._(); @@ -27,13 +29,26 @@ class FavoritenService { return songs.any((s) => s.id == songId); } - Future umschalten(int songId) async { - if (_favoritenPlaylistId == null) return; - if (await istFavorit(songId)) { - await _db.songAusPlaylistEntfernen(_favoritenPlaylistId!, songId); - } else { - final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!); - await _db.songZurPlaylist(_favoritenPlaylistId!, songId, songs.length); + /// 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 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); } }