feat: add Favorite Artists collection

- Add CollectionArtistEntry model with toJson/fromJson and artistCollectionKey helper
- Create favorite_artists table in SQLite with DB migration v1→v2
- Implement toggleFavoriteArtist/removeFavoriteArtist in LibraryCollectionsNotifier
- Add FavoriteArtistsScreen with list view, empty state, and artist navigation
- Add heart toggle button on ArtistScreen header (reactive via Riverpod selector)
- Integrate favorite artists folder in queue_tab collection grid/list views
- Add 8 new localization strings across all 13 locale files
This commit is contained in:
zarzet
2026-05-02 00:50:02 +07:00
parent 148e5c1231
commit 64dbf4441c
21 changed files with 1112 additions and 2 deletions
+48
View File
@@ -4475,6 +4475,12 @@ abstract class AppLocalizations {
/// **'Loved'**
String get collectionLoved;
/// Custom folder for favorite artists
///
/// In en, this message translates to:
/// **'Favorite Artists'**
String get collectionFavoriteArtists;
/// Custom user playlists folder
///
/// In en, this message translates to:
@@ -4517,6 +4523,12 @@ abstract class AppLocalizations {
/// **'{count, plural, =1{1 track} other{{count} tracks}}'**
String collectionPlaylistTracks(int count);
/// Artist count label for favorite artists
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 artist} other{{count} artists}}'**
String collectionArtistCount(int count);
/// Snackbar after adding track to playlist
///
/// In en, this message translates to:
@@ -4601,6 +4613,18 @@ abstract class AppLocalizations {
/// **'Tap love on tracks to keep your favorites'**
String get collectionLovedEmptySubtitle;
/// Favorite artists empty state title
///
/// In en, this message translates to:
/// **'No favorite artists yet'**
String get collectionFavoriteArtistsEmptyTitle;
/// Favorite artists empty state subtitle
///
/// In en, this message translates to:
/// **'Tap the heart on an artist page to keep them here'**
String get collectionFavoriteArtistsEmptySubtitle;
/// Playlist empty state title
///
/// In en, this message translates to:
@@ -4655,6 +4679,18 @@ abstract class AppLocalizations {
/// **'\"{trackName}\" removed from Wishlist'**
String collectionRemovedFromWishlist(String trackName);
/// Snackbar after adding artist to favorite artists
///
/// In en, this message translates to:
/// **'\"{artistName}\" added to Favorite Artists'**
String collectionAddedToFavoriteArtists(String artistName);
/// Snackbar after removing artist from favorite artists
///
/// In en, this message translates to:
/// **'\"{artistName}\" removed from Favorite Artists'**
String collectionRemovedFromFavoriteArtists(String artistName);
/// Bottom sheet action label - add track to loved folder
///
/// In en, this message translates to:
@@ -4679,6 +4715,18 @@ abstract class AppLocalizations {
/// **'Remove from Wishlist'**
String get trackOptionRemoveFromWishlist;
/// Action label - add artist to favorite artists
///
/// In en, this message translates to:
/// **'Add to Favorite Artists'**
String get artistOptionAddToFavorites;
/// Action label - remove artist from favorite artists
///
/// In en, this message translates to:
/// **'Remove from Favorite Artists'**
String get artistOptionRemoveFromFavorites;
/// Bottom sheet action to pick a custom cover image for a playlist
///
/// In en, this message translates to:
+37
View File
@@ -2549,6 +2549,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get collectionLoved => 'Lieblingssongs';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlisten';
@@ -2579,6 +2582,17 @@ class AppLocalizationsDe extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Zu \"$playlistName \" hinzugefügt';
@@ -2629,6 +2643,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tippe auf das Herz, um deine Favoriten zu behalten';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Die Playlist ist leer';
@@ -2667,6 +2688,16 @@ class AppLocalizationsDe extends AppLocalizations {
return '\"$trackName\" aus der Wunschliste entfernt';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Zu Lieblingssongs hinzufügen';
@@ -2679,6 +2710,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Von der Wunschliste entfernen';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Coverbild ändern';
+37
View File
@@ -2518,6 +2518,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2548,6 +2551,17 @@ class AppLocalizationsEn extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2598,6 +2612,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2636,6 +2657,16 @@ class AppLocalizationsEn extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2648,6 +2679,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2518,6 +2518,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2548,6 +2551,17 @@ class AppLocalizationsEs extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2598,6 +2612,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2636,6 +2657,16 @@ class AppLocalizationsEs extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2648,6 +2679,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2519,6 +2519,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2549,6 +2552,17 @@ class AppLocalizationsFr extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2599,6 +2613,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2637,6 +2658,16 @@ class AppLocalizationsFr extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2649,6 +2680,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2517,6 +2517,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2547,6 +2550,17 @@ class AppLocalizationsHi extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2597,6 +2611,13 @@ class AppLocalizationsHi extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2635,6 +2656,16 @@ class AppLocalizationsHi extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2647,6 +2678,12 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2527,6 +2527,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2557,6 +2560,17 @@ class AppLocalizationsId extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2607,6 +2621,13 @@ class AppLocalizationsId extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2645,6 +2666,16 @@ class AppLocalizationsId extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2657,6 +2688,12 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2503,6 +2503,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2533,6 +2536,17 @@ class AppLocalizationsJa extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2583,6 +2597,13 @@ class AppLocalizationsJa extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2621,6 +2642,16 @@ class AppLocalizationsJa extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2633,6 +2664,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'ウィッシュから削除';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'カバー画像を変更';
+37
View File
@@ -2497,6 +2497,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2527,6 +2530,17 @@ class AppLocalizationsKo extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2577,6 +2591,13 @@ class AppLocalizationsKo extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2615,6 +2636,16 @@ class AppLocalizationsKo extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2627,6 +2658,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2517,6 +2517,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2547,6 +2550,17 @@ class AppLocalizationsNl extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2597,6 +2611,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2635,6 +2656,16 @@ class AppLocalizationsNl extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2647,6 +2678,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2518,6 +2518,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2548,6 +2551,17 @@ class AppLocalizationsPt extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2598,6 +2612,13 @@ class AppLocalizationsPt extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2636,6 +2657,16 @@ class AppLocalizationsPt extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2648,6 +2679,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+37
View File
@@ -2570,6 +2570,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get collectionLoved => 'Любимые';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Плейлисты';
@@ -2602,6 +2605,17 @@ class AppLocalizationsRu extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Добавлено в \"$playlistName\"';
@@ -2652,6 +2666,13 @@ class AppLocalizationsRu extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Нажмите \"любовь\" на треках, чтобы сохранить ваши избранные';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Плейлист пуст';
@@ -2690,6 +2711,16 @@ class AppLocalizationsRu extends AppLocalizations {
return '\"$trackName\" удалён из списка желаний';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Добавить в Любимое';
@@ -2702,6 +2733,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Удалить из списка желаний';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Изменить обложку';
+37
View File
@@ -2564,6 +2564,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get collectionLoved => 'Favoriler';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Çalma Listeleri';
@@ -2594,6 +2597,17 @@ class AppLocalizationsTr extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return '\"$playlistName\" listesine eklendi';
@@ -2645,6 +2659,13 @@ class AppLocalizationsTr extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Sevdiğiniz şarkıları burada toplamak için kalp ikonuna dokunun';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Bu çalma listesi boş';
@@ -2683,6 +2704,16 @@ class AppLocalizationsTr extends AppLocalizations {
return '\"$trackName\" İstek Listenizden çıkarıldı';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Favorilere Ekle';
@@ -2695,6 +2726,12 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'İstek Listesinden Çıkar';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Kapak resmini değiştir';
+37
View File
@@ -2518,6 +2518,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get collectionLoved => 'Loved';
@override
String get collectionFavoriteArtists => 'Favorite Artists';
@override
String get collectionPlaylists => 'Playlists';
@@ -2548,6 +2551,17 @@ class AppLocalizationsZh extends AppLocalizations {
return '$_temp0';
}
@override
String collectionArtistCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count artists',
one: '1 artist',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
return 'Added to \"$playlistName\"';
@@ -2598,6 +2612,13 @@ class AppLocalizationsZh extends AppLocalizations {
String get collectionLovedEmptySubtitle =>
'Tap love on tracks to keep your favorites';
@override
String get collectionFavoriteArtistsEmptyTitle => 'No favorite artists yet';
@override
String get collectionFavoriteArtistsEmptySubtitle =>
'Tap the heart on an artist page to keep them here';
@override
String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@@ -2636,6 +2657,16 @@ class AppLocalizationsZh extends AppLocalizations {
return '\"$trackName\" removed from Wishlist';
}
@override
String collectionAddedToFavoriteArtists(String artistName) {
return '\"$artistName\" added to Favorite Artists';
}
@override
String collectionRemovedFromFavoriteArtists(String artistName) {
return '\"$artistName\" removed from Favorite Artists';
}
@override
String get trackOptionAddToLoved => 'Add to Loved';
@@ -2648,6 +2679,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
String get artistOptionAddToFavorites => 'Add to Favorite Artists';
@override
String get artistOptionRemoveFromFavorites => 'Remove from Favorite Artists';
@override
String get collectionPlaylistChangeCover => 'Change cover image';
+47
View File
@@ -3330,6 +3330,10 @@
"@collectionLoved": {
"description": "Custom folder for favorite tracks"
},
"collectionFavoriteArtists": "Favorite Artists",
"@collectionFavoriteArtists": {
"description": "Custom folder for favorite artists"
},
"collectionPlaylists": "Playlists",
"@collectionPlaylists": {
"description": "Custom user playlists folder"
@@ -3363,6 +3367,15 @@
}
}
},
"collectionArtistCount": "{count, plural, =1{1 artist} other{{count} artists}}",
"@collectionArtistCount": {
"description": "Artist count label for favorite artists",
"placeholders": {
"count": {
"type": "int"
}
}
},
"collectionAddedToPlaylist": "Added to \"{playlistName}\"",
"@collectionAddedToPlaylist": {
"description": "Snackbar after adding track to playlist",
@@ -3434,6 +3447,14 @@
"@collectionLovedEmptySubtitle": {
"description": "Loved empty state subtitle"
},
"collectionFavoriteArtistsEmptyTitle": "No favorite artists yet",
"@collectionFavoriteArtistsEmptyTitle": {
"description": "Favorite artists empty state title"
},
"collectionFavoriteArtistsEmptySubtitle": "Tap the heart on an artist page to keep them here",
"@collectionFavoriteArtistsEmptySubtitle": {
"description": "Favorite artists empty state subtitle"
},
"collectionPlaylistEmptyTitle": "Playlist is empty",
"@collectionPlaylistEmptyTitle": {
"description": "Playlist empty state title"
@@ -3495,6 +3516,24 @@
}
}
},
"collectionAddedToFavoriteArtists": "\"{artistName}\" added to Favorite Artists",
"@collectionAddedToFavoriteArtists": {
"description": "Snackbar after adding artist to favorite artists",
"placeholders": {
"artistName": {
"type": "String"
}
}
},
"collectionRemovedFromFavoriteArtists": "\"{artistName}\" removed from Favorite Artists",
"@collectionRemovedFromFavoriteArtists": {
"description": "Snackbar after removing artist from favorite artists",
"placeholders": {
"artistName": {
"type": "String"
}
}
},
"trackOptionAddToLoved": "Add to Loved",
"@trackOptionAddToLoved": {
"description": "Bottom sheet action label - add track to loved folder"
@@ -3511,6 +3550,14 @@
"@trackOptionRemoveFromWishlist": {
"description": "Bottom sheet action label - remove track from wishlist"
},
"artistOptionAddToFavorites": "Add to Favorite Artists",
"@artistOptionAddToFavorites": {
"description": "Action label - add artist to favorite artists"
},
"artistOptionRemoveFromFavorites": "Remove from Favorite Artists",
"@artistOptionRemoveFromFavorites": {
"description": "Action label - remove artist from favorite artists"
},
"collectionPlaylistChangeCover": "Change cover image",
"@collectionPlaylistChangeCover": {
"description": "Bottom sheet action to pick a custom cover image for a playlist"
@@ -19,6 +19,28 @@ String trackCollectionKey(Track track) {
return '$source:${track.id}';
}
String _stripCollectionResourcePrefix(String value) {
final colonIndex = value.indexOf(':');
if (colonIndex <= 0 || colonIndex == value.length - 1) {
return value.trim();
}
return value.substring(colonIndex + 1).trim();
}
String artistCollectionKey({
required String artistId,
required String? providerId,
}) {
final trimmedArtistId = artistId.trim();
final trimmedProviderId = providerId?.trim();
final source = trimmedProviderId != null && trimmedProviderId.isNotEmpty
? trimmedProviderId.toLowerCase()
: (trimmedArtistId.contains(':')
? trimmedArtistId.split(':').first.toLowerCase()
: 'builtin');
return '$source:${_stripCollectionResourcePrefix(trimmedArtistId)}';
}
class CollectionTrackEntry {
final String key;
final Track track;
@@ -46,6 +68,49 @@ class CollectionTrackEntry {
}
}
class CollectionArtistEntry {
final String key;
final String artistId;
final String? providerId;
final String name;
final String? imageUrl;
final DateTime addedAt;
const CollectionArtistEntry({
required this.key,
required this.artistId,
required this.providerId,
required this.name,
this.imageUrl,
required this.addedAt,
});
Map<String, dynamic> toJson() => {
'key': key,
'artistId': artistId,
'providerId': providerId,
'name': name,
'imageUrl': imageUrl,
'addedAt': addedAt.toIso8601String(),
};
factory CollectionArtistEntry.fromJson(Map<String, dynamic> json) {
final artistId = json['artistId'] as String;
final providerId = json['providerId'] as String?;
final addedAtRaw = json['addedAt'] as String?;
return CollectionArtistEntry(
key:
json['key'] as String? ??
artistCollectionKey(artistId: artistId, providerId: providerId),
artistId: artistId,
providerId: providerId,
name: json['name'] as String? ?? '',
imageUrl: json['imageUrl'] as String?,
addedAt: DateTime.tryParse(addedAtRaw ?? '') ?? DateTime.now(),
);
}
}
class UserPlaylistCollection {
final String id;
final String name;
@@ -180,9 +245,11 @@ class LibraryCollectionsState {
final List<CollectionTrackEntry> wishlist;
final List<CollectionTrackEntry> loved;
final List<UserPlaylistCollection> playlists;
final List<CollectionArtistEntry> favoriteArtists;
final bool isLoaded;
final Set<String> _wishlistKeys;
final Set<String> _lovedKeys;
final Set<String> _favoriteArtistKeys;
final Map<String, UserPlaylistCollection> _playlistsById;
final Set<String> _allPlaylistTrackKeys;
@@ -190,14 +257,19 @@ class LibraryCollectionsState {
this.wishlist = const [],
this.loved = const [],
this.playlists = const [],
this.favoriteArtists = const [],
this.isLoaded = false,
Set<String>? wishlistKeys,
Set<String>? lovedKeys,
Set<String>? favoriteArtistKeys,
Map<String, UserPlaylistCollection>? playlistsById,
Set<String>? allPlaylistTrackKeys,
}) : _wishlistKeys =
wishlistKeys ?? wishlist.map((entry) => entry.key).toSet(),
_lovedKeys = lovedKeys ?? loved.map((entry) => entry.key).toSet(),
_favoriteArtistKeys =
favoriteArtistKeys ??
favoriteArtists.map((entry) => entry.key).toSet(),
_playlistsById =
playlistsById ??
Map.fromEntries(
@@ -209,6 +281,7 @@ class LibraryCollectionsState {
int get wishlistCount => wishlist.length;
int get lovedCount => loved.length;
int get playlistCount => playlists.length;
int get favoriteArtistCount => favoriteArtists.length;
bool isInWishlist(Track track) {
final key = trackCollectionKey(track);
@@ -228,6 +301,18 @@ class LibraryCollectionsState {
return _lovedKeys.contains(trackKey);
}
bool isFavoriteArtist({
required String artistId,
required String? providerId,
}) {
final key = artistCollectionKey(artistId: artistId, providerId: providerId);
return _favoriteArtistKeys.contains(key);
}
bool containsFavoriteArtistKey(String artistKey) {
return _favoriteArtistKeys.contains(artistKey);
}
UserPlaylistCollection? playlistById(String playlistId) {
return _playlistsById[playlistId];
}
@@ -248,22 +333,30 @@ class LibraryCollectionsState {
List<CollectionTrackEntry>? wishlist,
List<CollectionTrackEntry>? loved,
List<UserPlaylistCollection>? playlists,
List<CollectionArtistEntry>? favoriteArtists,
bool? isLoaded,
}) {
final nextWishlist = wishlist ?? this.wishlist;
final nextLoved = loved ?? this.loved;
final nextPlaylists = playlists ?? this.playlists;
final nextFavoriteArtists = favoriteArtists ?? this.favoriteArtists;
final keepWishlistIndex = identical(nextWishlist, this.wishlist);
final keepLovedIndex = identical(nextLoved, this.loved);
final keepPlaylistIndex = identical(nextPlaylists, this.playlists);
final keepFavoriteArtistIndex = identical(
nextFavoriteArtists,
this.favoriteArtists,
);
return LibraryCollectionsState(
wishlist: nextWishlist,
loved: nextLoved,
playlists: nextPlaylists,
favoriteArtists: nextFavoriteArtists,
isLoaded: isLoaded ?? this.isLoaded,
wishlistKeys: keepWishlistIndex ? _wishlistKeys : null,
lovedKeys: keepLovedIndex ? _lovedKeys : null,
favoriteArtistKeys: keepFavoriteArtistIndex ? _favoriteArtistKeys : null,
playlistsById: keepPlaylistIndex ? _playlistsById : null,
allPlaylistTrackKeys: keepPlaylistIndex ? _allPlaylistTrackKeys : null,
);
@@ -273,12 +366,14 @@ class LibraryCollectionsState {
'wishlist': wishlist.map((e) => e.toJson()).toList(),
'loved': loved.map((e) => e.toJson()).toList(),
'playlists': playlists.map((e) => e.toJson()).toList(),
'favoriteArtists': favoriteArtists.map((e) => e.toJson()).toList(),
};
factory LibraryCollectionsState.fromJson(Map<String, dynamic> json) {
final wishlistRaw = (json['wishlist'] as List?) ?? const [];
final lovedRaw = (json['loved'] as List?) ?? const [];
final playlistsRaw = (json['playlists'] as List?) ?? const [];
final favoriteArtistsRaw = (json['favoriteArtists'] as List?) ?? const [];
return LibraryCollectionsState(
wishlist: wishlistRaw
@@ -300,6 +395,12 @@ class LibraryCollectionsState {
UserPlaylistCollection.fromJson(Map<String, dynamic>.from(e)),
)
.toList(growable: false),
favoriteArtists: favoriteArtistsRaw
.whereType<Map<Object?, Object?>>()
.map(
(e) => CollectionArtistEntry.fromJson(Map<String, dynamic>.from(e)),
)
.toList(growable: false),
isLoaded: true,
);
}
@@ -360,6 +461,14 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
}
}
final favoriteArtists = <CollectionArtistEntry>[];
for (final row in snapshot.favoriteArtistRows) {
final parsed = _parseArtistEntryRow(row);
if (parsed != null) {
favoriteArtists.add(parsed);
}
}
final tracksByPlaylist = <String, List<CollectionTrackEntry>>{};
for (final row in snapshot.playlistTrackRows) {
final playlistId = row['playlist_id'] as String?;
@@ -396,6 +505,7 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
wishlist: wishlist,
loved: loved,
playlists: playlists,
favoriteArtists: favoriteArtists,
isLoaded: true,
);
} catch (_) {
@@ -430,6 +540,31 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
}
}
CollectionArtistEntry? _parseArtistEntryRow(Map<String, dynamic> row) {
final key = row['artist_key'] as String?;
final artistJson = row['artist_json'] as String?;
if (key == null ||
key.isEmpty ||
artistJson == null ||
artistJson.isEmpty) {
return null;
}
try {
final decoded = jsonDecode(artistJson);
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
final addedAtRaw = row['added_at'] as String?;
return CollectionArtistEntry.fromJson({
...map,
'key': key,
'addedAt': map['addedAt'] ?? addedAtRaw,
});
} catch (_) {
return null;
}
}
bool _replacePlaylistById(
String playlistId,
UserPlaylistCollection Function(UserPlaylistCollection playlist) update,
@@ -503,6 +638,59 @@ class LibraryCollectionsNotifier extends Notifier<LibraryCollectionsState> {
return true;
}
Future<bool> toggleFavoriteArtist({
required String artistId,
required String? providerId,
required String name,
String? imageUrl,
}) async {
await _ensureLoaded();
final key = artistCollectionKey(artistId: artistId, providerId: providerId);
final sourceSeparator = key.indexOf(':');
final source = sourceSeparator > 0 ? key.substring(0, sourceSeparator) : '';
final trimmedProviderId = providerId?.trim();
final effectiveProviderId =
trimmedProviderId != null && trimmedProviderId.isNotEmpty
? trimmedProviderId
: (source.isNotEmpty && source != 'builtin' ? source : null);
if (state.containsFavoriteArtistKey(key)) {
await _db.deleteFavoriteArtistEntry(key);
final updated = state.favoriteArtists
.where((entry) => entry.key != key)
.toList(growable: false);
state = state.copyWith(favoriteArtists: updated);
return false;
}
final entry = CollectionArtistEntry(
key: key,
artistId: _stripCollectionResourcePrefix(artistId),
providerId: effectiveProviderId,
name: name,
imageUrl: imageUrl,
addedAt: DateTime.now(),
);
await _db.upsertFavoriteArtistEntry(
artistKey: key,
artistJson: jsonEncode(entry.toJson()),
addedAt: entry.addedAt.toIso8601String(),
);
final updated = [entry, ...state.favoriteArtists];
state = state.copyWith(favoriteArtists: updated);
return true;
}
Future<void> removeFavoriteArtist(String artistKey) async {
await _ensureLoaded();
if (!state.containsFavoriteArtistKey(artistKey)) return;
await _db.deleteFavoriteArtistEntry(artistKey);
final updated = state.favoriteArtists
.where((entry) => entry.key != artistKey)
.toList(growable: false);
state = state.copyWith(favoriteArtists: updated);
}
Future<void> removeFromWishlist(String trackKey) async {
await _ensureLoaded();
if (!state.containsWishlistKey(trackKey)) return;
+64
View File
@@ -10,6 +10,7 @@ import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/track_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/providers/recent_access_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
@@ -912,6 +913,32 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
await _downloadAlbums(context, albums);
}
Future<void> _toggleFavoriteArtist(BuildContext context) async {
final providerId = _directMetadataProviderId();
final imageUrl =
_headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl;
final added = await ref
.read(libraryCollectionsProvider.notifier)
.toggleFavoriteArtist(
artistId: _metadataResourceId(providerId ?? ''),
providerId: providerId,
name: widget.artistName,
imageUrl: imageUrl,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
added
? context.l10n.collectionAddedToFavoriteArtists(widget.artistName)
: context.l10n.collectionRemovedFromFavoriteArtists(
widget.artistName,
),
),
),
);
}
Future<void> _fetchAndQueueAlbums(
List<ArtistAlbum> albums,
String service,
@@ -1100,6 +1127,17 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
);
}
final favoriteProviderId = _directMetadataProviderId();
final favoriteArtistId = _metadataResourceId(favoriteProviderId ?? '');
final isFavoriteArtist = ref.watch(
libraryCollectionsProvider.select(
(state) => state.isFavoriteArtist(
artistId: favoriteArtistId,
providerId: favoriteProviderId,
),
),
);
return SliverAppBar(
expandedHeight: hasDiscography ? 420 : 380,
pinned: true,
@@ -1220,6 +1258,32 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
],
),
),
if (!_isSelectionMode) ...[
const SizedBox(width: 12),
Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _toggleFavoriteArtist(context),
icon: Icon(
isFavoriteArtist
? Icons.favorite
: Icons.favorite_border,
size: 26,
),
color: isFavoriteArtist
? colorScheme.error
: Colors.black87,
tooltip: isFavoriteArtist
? context.l10n.artistOptionRemoveFromFavorites
: context.l10n.artistOptionAddToFavorites,
),
),
],
if (hasDiscography && !_isSelectionMode) ...[
const SizedBox(width: 12),
Container(
+197
View File
@@ -0,0 +1,197 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/screens/artist_screen.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
class FavoriteArtistsScreen extends ConsumerWidget {
const FavoriteArtistsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final artists = ref.watch(
libraryCollectionsProvider.select((state) => state.favoriteArtists),
);
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 120 + topPadding,
collapsedHeight: kToolbarHeight,
floating: false,
pinned: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
leading: IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final maxHeight = 120 + topPadding;
final minHeight = kToolbarHeight + topPadding;
final expandRatio =
((constraints.maxHeight - minHeight) /
(maxHeight - minHeight))
.clamp(0.0, 1.0);
final leftPadding = 56 - (32 * expandRatio);
return FlexibleSpaceBar(
expandedTitleScale: 1.0,
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
title: Text(
context.l10n.collectionFavoriteArtists,
style: TextStyle(
fontSize: 20 + (8 * expandRatio),
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
);
},
),
),
if (artists.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.favorite,
size: 60,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 12),
Text(
context.l10n.collectionFavoriteArtistsEmptyTitle,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
context.l10n.collectionFavoriteArtistsEmptySubtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
),
)
else
SliverList.separated(
itemCount: artists.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final artist = artists[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
leading: _ArtistThumbnail(artist: artist),
title: Text(
artist.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle:
artist.providerId == null || artist.providerId!.isEmpty
? null
: Text(
artist.providerId!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
tooltip: context.l10n.artistOptionRemoveFromFavorites,
icon: Icon(Icons.favorite, color: colorScheme.error),
onPressed: () async {
await ref
.read(libraryCollectionsProvider.notifier)
.removeFavoriteArtist(artist.key);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.collectionRemovedFromFavoriteArtists(
artist.name,
),
),
),
);
},
),
onTap: () {
Navigator.of(context).push(
slidePageRoute<void>(
page: ArtistScreen(
artistId: artist.artistId,
artistName: artist.name,
coverUrl: artist.imageUrl,
extensionId:
artist.providerId != null &&
artist.providerId!.isNotEmpty
? artist.providerId
: null,
),
),
);
},
);
},
),
],
),
);
}
}
class _ArtistThumbnail extends StatelessWidget {
final CollectionArtistEntry artist;
const _ArtistThumbnail({required this.artist});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final imageUrl = artist.imageUrl;
return ClipOval(
child: imageUrl != null && imageUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: imageUrl,
width: 56,
height: 56,
fit: BoxFit.cover,
memCacheWidth: 112,
memCacheHeight: 112,
cacheManager: CoverCacheManager.instance,
errorWidget: (_, _, _) => _placeholder(colorScheme),
)
: _placeholder(colorScheme),
);
}
Widget _placeholder(ColorScheme colorScheme) {
return Container(
width: 56,
height: 56,
color: colorScheme.surfaceContainerHighest,
child: Icon(Icons.person, color: colorScheme.onSurfaceVariant),
);
}
}
+34
View File
@@ -27,6 +27,7 @@ import 'package:spotiflac_android/services/local_track_redownload_service.dart';
import 'package:spotiflac_android/services/history_database.dart';
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/screens/favorite_artists_screen.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
@@ -2029,6 +2030,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
void _openFavoriteArtistsFolder() {
_navigateWithUnfocus(
MaterialPageRoute(builder: (_) => const FavoriteArtistsScreen()),
);
}
void _openPlaylistById(String playlistId) {
_navigateWithUnfocus(
MaterialPageRoute(
@@ -2315,6 +2322,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
(s) => (
s.wishlistCount,
s.lovedCount,
s.favoriteArtistCount,
s.playlistCount,
s.hasPlaylistTracks,
s.isLoaded,
@@ -2932,6 +2940,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
if (collectionState.lovedCount > 0) {
entries.add(_CollectionEntry.loved);
}
if (collectionState.favoriteArtistCount > 0) {
entries.add(_CollectionEntry.favoriteArtists);
}
for (var i = 0; i < collectionState.playlists.length; i++) {
entries.add(_CollectionEntry.playlist(i));
}
@@ -2968,6 +2979,17 @@ class _QueueTabState extends ConsumerState<QueueTab> {
count: collectionState.lovedCount,
onTap: _openLovedFolder,
);
case _CollectionEntryType.favoriteArtists:
return _buildCollectionGridItem(
context: context,
colorScheme: colorScheme,
icon: Icons.person,
iconColor: Colors.white,
iconBgColor: const Color(0xFFE91E63),
title: context.l10n.collectionFavoriteArtists,
count: collectionState.favoriteArtistCount,
onTap: _openFavoriteArtistsFolder,
);
case _CollectionEntryType.playlist:
final playlist = collectionState.playlists[entry.playlistIndex];
final isSelected = _selectedPlaylistIds.contains(playlist.id);
@@ -3087,6 +3109,18 @@ class _QueueTabState extends ConsumerState<QueueTab> {
'${context.l10n.collectionFoldersTitle}${collectionState.lovedCount} ${collectionState.lovedCount == 1 ? 'track' : 'tracks'}',
onTap: _openLovedFolder,
);
case _CollectionEntryType.favoriteArtists:
return _buildCollectionListItem(
context: context,
colorScheme: colorScheme,
icon: Icons.person,
iconColor: Colors.white,
iconBgColor: const Color(0xFFE91E63),
title: context.l10n.collectionFavoriteArtists,
subtitle:
'${context.l10n.collectionFoldersTitle}${context.l10n.collectionArtistCount(collectionState.favoriteArtistCount)}',
onTap: _openFavoriteArtistsFolder,
);
case _CollectionEntryType.playlist:
final playlist = collectionState.playlists[entry.playlistIndex];
final isSelected = _selectedPlaylistIds.contains(playlist.id);
+4 -1
View File
@@ -25,7 +25,7 @@ class _QueueItemSliverRow extends ConsumerWidget {
}
}
enum _CollectionEntryType { wishlist, loved, playlist }
enum _CollectionEntryType { wishlist, loved, favoriteArtists, playlist }
class _CollectionEntry {
final _CollectionEntryType type;
@@ -35,6 +35,9 @@ class _CollectionEntry {
static const wishlist = _CollectionEntry._(_CollectionEntryType.wishlist);
static const loved = _CollectionEntry._(_CollectionEntryType.loved);
static const favoriteArtists = _CollectionEntry._(
_CollectionEntryType.favoriteArtists,
);
static _CollectionEntry playlist(int index) =>
_CollectionEntry._(_CollectionEntryType.playlist, index);
}
+49 -1
View File
@@ -9,12 +9,13 @@ import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('LibraryCollectionsDb');
const _dbFileName = 'library_collections.db';
const _dbVersion = 1;
const _dbVersion = 2;
const _tableWishlist = 'wishlist_tracks';
const _tableLoved = 'loved_tracks';
const _tablePlaylists = 'playlists';
const _tablePlaylistTracks = 'playlist_tracks';
const _tableFavoriteArtists = 'favorite_artists';
const _legacyCollectionsStorageKey = 'library_collections_v1';
const _migrationDoneKey = 'library_collections_migrated_to_sqlite_v1';
@@ -24,12 +25,14 @@ class LibraryCollectionsSnapshot {
final List<Map<String, dynamic>> lovedRows;
final List<Map<String, dynamic>> playlistRows;
final List<Map<String, dynamic>> playlistTrackRows;
final List<Map<String, dynamic>> favoriteArtistRows;
const LibraryCollectionsSnapshot({
required this.wishlistRows,
required this.lovedRows,
required this.playlistRows,
required this.playlistTrackRows,
required this.favoriteArtistRows,
});
}
@@ -129,6 +132,8 @@ class LibraryCollectionsDatabase {
)
''');
await _createFavoriteArtistsTable(db);
await db.execute(
'CREATE INDEX idx_${_tableWishlist}_added_at ON $_tableWishlist(added_at DESC)',
);
@@ -148,6 +153,22 @@ class LibraryCollectionsDatabase {
Future<void> _upgradeDb(Database db, int oldVersion, int newVersion) async {
_log.i('Upgrading collections database from v$oldVersion to v$newVersion');
if (oldVersion < 2) {
await _createFavoriteArtistsTable(db);
}
}
Future<void> _createFavoriteArtistsTable(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $_tableFavoriteArtists (
artist_key TEXT PRIMARY KEY,
artist_json TEXT NOT NULL,
added_at TEXT NOT NULL
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_${_tableFavoriteArtists}_added_at ON $_tableFavoriteArtists(added_at DESC)',
);
}
Future<bool> migrateFromSharedPreferences() async {
@@ -264,12 +285,17 @@ class LibraryCollectionsDatabase {
_tablePlaylistTracks,
orderBy: 'playlist_id ASC, added_at DESC, rowid DESC',
);
final favoriteArtistRows = await db.query(
_tableFavoriteArtists,
orderBy: 'added_at DESC, rowid DESC',
);
return LibraryCollectionsSnapshot(
wishlistRows: wishlistRows,
lovedRows: lovedRows,
playlistRows: playlistRows,
playlistTrackRows: playlistTrackRows,
favoriteArtistRows: favoriteArtistRows,
);
}
@@ -426,6 +452,28 @@ class LibraryCollectionsDatabase {
await db.delete(_tableLoved, where: 'track_key = ?', whereArgs: [trackKey]);
}
Future<void> upsertFavoriteArtistEntry({
required String artistKey,
required String artistJson,
required String addedAt,
}) async {
final db = await database;
await db.insert(_tableFavoriteArtists, {
'artist_key': artistKey,
'artist_json': artistJson,
'added_at': addedAt,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> deleteFavoriteArtistEntry(String artistKey) async {
final db = await database;
await db.delete(
_tableFavoriteArtists,
where: 'artist_key = ?',
whereArgs: [artistKey],
);
}
Future<void> upsertPlaylist({
required String id,
required String name,