From db99e2b1f7ba87d8bbc2e0fa4b7a3909d973974d Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Tue, 18 Aug 2026 15:29:51 +0200 Subject: [PATCH] feat(ui): add favorite heart toggle, wire into song list - Filled heart when favorited, outline when not (reactive via watchIsFavorite) - Wired into every song list tile (library, search, playlists) Co-Authored-By: Claude Haiku 4.5 --- lib/library/song_list.dart | 2 ++ lib/shared/favorite_button.dart | 31 +++++++++++++++++++++ test/shared/favorite_button_test.dart | 39 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 lib/shared/favorite_button.dart create mode 100644 test/shared/favorite_button_test.dart diff --git a/lib/library/song_list.dart b/lib/library/song_list.dart index 488b45e..af63727 100644 --- a/lib/library/song_list.dart +++ b/lib/library/song_list.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../player/audio_handler.dart'; import '../shared/cover.dart'; +import '../shared/favorite_button.dart'; import 'database.dart'; import 'song_media.dart'; @@ -27,6 +28,7 @@ class SongList extends StatelessWidget { title: Text(s.title, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Text(s.artist ?? 'Unbekannt', maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: FavoriteButton(songId: s.id), onTap: () async { try { await playSongs(handler, songs, i); diff --git a/lib/shared/favorite_button.dart b/lib/shared/favorite_button.dart new file mode 100644 index 0000000..88f6b88 --- /dev/null +++ b/lib/shared/favorite_button.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../library/database.dart'; +import '../library/playlist_service.dart'; +import 'theme.dart'; + +/// Herz-Toggle: ausgefüllt = favorisiert, umrandet = nicht favorisiert. +class FavoriteButton extends StatelessWidget { + const FavoriteButton({super.key, required this.songId}); + final String songId; + + @override + Widget build(BuildContext context) { + final db = context.read(); + final service = context.read(); + return StreamBuilder( + stream: db.watchIsFavorite(songId), + builder: (context, snapshot) { + final isFavorite = snapshot.data ?? false; + return IconButton( + tooltip: isFavorite ? 'Favorit entfernen' : 'Zu Favoriten hinzufügen', + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? MeloTheme.red : Colors.white54, + ), + onPressed: () => service.toggleFavorite(songId), + ); + }, + ); + } +} diff --git a/test/shared/favorite_button_test.dart b/test/shared/favorite_button_test.dart new file mode 100644 index 0000000..9f15c27 --- /dev/null +++ b/test/shared/favorite_button_test.dart @@ -0,0 +1,39 @@ +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:melo/library/database.dart'; +import 'package:melo/library/playlist_service.dart'; +import 'package:melo/shared/favorite_button.dart'; + +void main() { + testWidgets('shows outlined heart when not favorite, fills on tap', (tester) async { + final db = MeloDb(NativeDatabase.memory()); + final service = PlaylistService(db); + await db.into(db.songs).insert(SongsCompanion.insert( + id: 'song-1', path: '/a.mp3', title: 'A', dateAddedMs: 0, updatedAtMs: 0, + )); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: db), + ChangeNotifierProvider.value(value: service), + ], + child: MaterialApp( + home: Scaffold(body: FavoriteButton(songId: 'song-1')), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.favorite_border), findsOneWidget); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.favorite), findsOneWidget); + + await db.close(); + }); +}