From ef3e019a3aee523b28118bd08b474c422ecaa179 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 27 Jul 2026 02:12:35 +0700 Subject: [PATCH] feat(library): playlists filter view and configurable default view Add a fourth Playlists chip/page to the Library filter pager, rendered as an album-style grid of playlist cells (search filters by name, page provider short-circuits since playlists come from libraryCollections). Add a defaultLibraryView setting (last used / all / albums / singles / playlists) in Library settings, applied on startup and whenever the shell switches back to the Library tab. --- lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_de.dart | 21 +++++ lib/l10n/app_localizations_en.dart | 21 +++++ lib/l10n/app_localizations_es.dart | 21 +++++ lib/l10n/app_localizations_fr.dart | 21 +++++ lib/l10n/app_localizations_id.dart | 21 +++++ lib/l10n/app_localizations_ja.dart | 21 +++++ lib/l10n/app_localizations_ko.dart | 21 +++++ lib/l10n/app_localizations_pt.dart | 21 +++++ lib/l10n/app_localizations_ru.dart | 21 +++++ lib/l10n/app_localizations_tr.dart | 21 +++++ lib/l10n/app_localizations_uk.dart | 21 +++++ lib/l10n/arb/app_en.arb | 21 +++++ lib/models/settings.dart | 7 ++ lib/models/settings.g.dart | 2 + lib/providers/settings_provider.dart | 23 +++++ lib/screens/queue_tab.dart | 58 ++++++++++++- lib/screens/queue_tab_filter_widgets.dart | 65 +++++++++++++- lib/screens/queue_tab_helpers.dart | 4 + .../settings/library_settings_page.dart | 85 +++++++++++++++++++ 20 files changed, 515 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ca09b7ed..67bc23a8 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5720,6 +5720,30 @@ abstract class AppLocalizations { /// **'Single track downloads will appear here'** String get queueEmptySinglesSubtitle; + /// Playlist count label with plural support + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** + String queuePlaylistCount(int count); + + /// Empty state subtitle for the playlists library view + /// + /// In en, this message translates to: + /// **'Create a playlist to organize your tracks'** + String get queueEmptyPlaylistsSubtitle; + + /// Setting title - which library view opens when switching to the Library tab + /// + /// In en, this message translates to: + /// **'Default view'** + String get libraryDefaultView; + + /// Default view option - keep the last used library view + /// + /// In en, this message translates to: + /// **'Last used'** + String get libraryDefaultViewLastUsed; + /// Empty state title when download history is empty /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b93bb920..ab99c5ae 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3466,6 +3466,27 @@ class AppLocalizationsDe extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Einzelne Titel-Downloads werden hier angezeigt'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'Kein Download-Verlauf'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 47ea2c63..8fbcde86 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3426,6 +3426,27 @@ class AppLocalizationsEn extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6cd39fe1..eebce9e4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3426,6 +3426,27 @@ class AppLocalizationsEs extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 43804375..1737e603 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3515,6 +3515,27 @@ class AppLocalizationsFr extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Les téléchargements de titres individuels apparaîtront ici'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'Aucun historique de téléchargement'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 2a157315..590a49fd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -3429,6 +3429,27 @@ class AppLocalizationsId extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 3857bc99..fad6f5af 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3415,6 +3415,27 @@ class AppLocalizationsJa extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 52af31e9..96460318 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3325,6 +3325,27 @@ class AppLocalizationsKo extends AppLocalizations { @override String get queueEmptySinglesSubtitle => '싱글 트랙 다운로드는 여기에 표시됩니다'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => '다운로드 기록이 없음'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 2e44cf73..49c1e273 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3426,6 +3426,27 @@ class AppLocalizationsPt extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 86bfd1f9..4357810e 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3455,6 +3455,27 @@ class AppLocalizationsRu extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'Нет истории скачиваний'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 5d3495bc..104f37de 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -3456,6 +3456,27 @@ class AppLocalizationsTr extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Single track downloads will appear here'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'No download history'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 51f217f0..8095b480 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3469,6 +3469,27 @@ class AppLocalizationsUk extends AppLocalizations { String get queueEmptySinglesSubtitle => 'Завантаження окремих треків з’являться тут'; + @override + String queuePlaylistCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playlists', + one: '1 playlist', + ); + return '$_temp0'; + } + + @override + String get queueEmptyPlaylistsSubtitle => + 'Create a playlist to organize your tracks'; + + @override + String get libraryDefaultView => 'Default view'; + + @override + String get libraryDefaultViewLastUsed => 'Last used'; + @override String get queueEmptyHistory => 'Немає історії завантажень'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f851a710..53ea1ba2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4415,6 +4415,27 @@ "@queueEmptySinglesSubtitle": { "description": "Empty state subtitle for single track downloads" }, + "queuePlaylistCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@queuePlaylistCount": { + "description": "Playlist count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueEmptyPlaylistsSubtitle": "Create a playlist to organize your tracks", + "@queueEmptyPlaylistsSubtitle": { + "description": "Empty state subtitle for the playlists library view" + }, + "libraryDefaultView": "Default view", + "@libraryDefaultView": { + "description": "Setting title - which library view opens when switching to the Library tab" + }, + "libraryDefaultViewLastUsed": "Last used", + "@libraryDefaultViewLastUsed": { + "description": "Default view option - keep the last used library view" + }, "queueEmptyHistory": "No download history", "@queueEmptyHistory": { "description": "Empty state title when download history is empty" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 17ec1ff0..f119fa84 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -34,6 +34,10 @@ class AppSettings { final bool filterContributingArtistsInAlbumArtist; final String historyViewMode; final String historyFilterMode; + + /// Library view opened when switching to the Library tab: + /// 'last' (keep last used), 'all', 'albums', 'singles', or 'playlists'. + final String defaultLibraryView; final bool askQualityBeforeDownload; final bool enableLogging; final bool useExtensionProviders; @@ -132,6 +136,7 @@ class AppSettings { this.filterContributingArtistsInAlbumArtist = false, this.historyViewMode = 'grid', this.historyFilterMode = 'all', + this.defaultLibraryView = 'last', this.askQualityBeforeDownload = true, this.enableLogging = false, this.useExtensionProviders = true, @@ -202,6 +207,7 @@ class AppSettings { bool? filterContributingArtistsInAlbumArtist, String? historyViewMode, String? historyFilterMode, + String? defaultLibraryView, bool? askQualityBeforeDownload, bool? enableLogging, bool? useExtensionProviders, @@ -279,6 +285,7 @@ class AppSettings { this.filterContributingArtistsInAlbumArtist, historyViewMode: historyViewMode ?? this.historyViewMode, historyFilterMode: historyFilterMode ?? this.historyFilterMode, + defaultLibraryView: defaultLibraryView ?? this.defaultLibraryView, askQualityBeforeDownload: askQualityBeforeDownload ?? this.askQualityBeforeDownload, enableLogging: enableLogging ?? this.enableLogging, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 38550d00..0704be88 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -33,6 +33,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( json['filterContributingArtistsInAlbumArtist'] as bool? ?? false, historyViewMode: json['historyViewMode'] as String? ?? 'grid', historyFilterMode: json['historyFilterMode'] as String? ?? 'all', + defaultLibraryView: json['defaultLibraryView'] as String? ?? 'last', askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true, enableLogging: json['enableLogging'] as bool? ?? false, useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, @@ -122,6 +123,7 @@ Map _$AppSettingsToJson( instance.filterContributingArtistsInAlbumArtist, 'historyViewMode': instance.historyViewMode, 'historyFilterMode': instance.historyFilterMode, + 'defaultLibraryView': instance.defaultLibraryView, 'askQualityBeforeDownload': instance.askQualityBeforeDownload, 'enableLogging': instance.enableLogging, 'useExtensionProviders': instance.useExtensionProviders, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 4c6473d9..8643534b 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -96,6 +96,13 @@ class SettingsNotifier extends Notifier { 'album', 'playlist', }; + static const Set _libraryViewValues = { + 'last', + 'all', + 'albums', + 'singles', + 'playlists', + }; static const Set _extensionVerificationBrowserModeValues = { 'external_first', 'in_app_first', @@ -149,6 +156,9 @@ class SettingsNotifier extends Notifier { loaded.downloadFallbackExtensionIds != null && sanitizedDownloadFallbackExtensionIds == null, defaultSearchTab: sanitizedDefaultSearchTab, + defaultLibraryView: _normalizeDefaultLibraryView( + loaded.defaultLibraryView, + ), defaultService: loaded.defaultService, searchProvider: loaded.searchProvider, extensionVerificationBrowserMode: @@ -353,6 +363,12 @@ class SettingsNotifier extends Notifier { return 'all'; } + String _normalizeDefaultLibraryView(String value) { + final normalized = value.trim().toLowerCase(); + if (_libraryViewValues.contains(normalized)) return normalized; + return 'last'; + } + String _normalizeExtensionVerificationBrowserMode(String value) { final normalized = value.trim().toLowerCase(); if (_extensionVerificationBrowserModeValues.contains(normalized)) { @@ -607,6 +623,13 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setDefaultLibraryView(String view) { + state = state.copyWith( + defaultLibraryView: _normalizeDefaultLibraryView(view), + ); + _saveSettings(); + } + void setHomeFeedProvider(String? provider) { if (provider == null || provider.isEmpty) { state = state.copyWith(clearHomeFeedProvider: true); diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 22ec76c6..30a67405 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -238,8 +238,9 @@ class _QueueTabState extends ConsumerState { double _playlistSelectionOverlayBottomPadding = 0; PageController? _filterPageController; - final List _filterModes = ['all', 'albums', 'singles']; + final List _filterModes = ['all', 'albums', 'singles', 'playlists']; bool _isPageControllerInitialized = false; + bool _wasTabVisible = false; static const List _months = [ 'Jan', 'Feb', @@ -327,11 +328,49 @@ class _QueueTabState extends ConsumerState { void _initializePageController() { if (_isPageControllerInitialized) return; _isPageControllerInitialized = true; - final currentFilter = ref.read(settingsProvider).historyFilterMode; - final initialPage = _filterModes.indexOf(currentFilter).clamp(0, 2); + final settings = ref.read(settingsProvider); + final initialFilter = settings.defaultLibraryView == 'last' + ? settings.historyFilterMode + : settings.defaultLibraryView; + final initialPage = _filterModes + .indexOf(initialFilter) + .clamp(0, _filterModes.length - 1); + if (settings.historyFilterMode != _filterModes[initialPage]) { + Future.microtask(() { + if (!mounted) return; + ref + .read(settingsProvider.notifier) + .setHistoryFilterMode(_filterModes[initialPage]); + }); + } _filterPageController = PageController(initialPage: initialPage); } + /// When the shell switches back to this tab and a fixed default view is + /// configured, jump the filter pager to it. + void _applyDefaultLibraryViewOnTabVisible() { + final isVisible = TickerMode.valuesOf(context).enabled; + final becameVisible = isVisible && !_wasTabVisible; + _wasTabVisible = isVisible; + if (!becameVisible) return; + final defaultView = ref.read(settingsProvider).defaultLibraryView; + if (defaultView == 'last') return; + final index = _filterModes.indexOf(defaultView); + if (index < 0) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (ref.read(settingsProvider).historyFilterMode != defaultView) { + ref.read(settingsProvider.notifier).setHistoryFilterMode(defaultView); + } + final controller = _filterPageController; + if (controller != null && + controller.hasClients && + controller.page?.round() != index) { + controller.jumpToPage(index); + } + }); + } + @override void dispose() { _hideSelectionOverlay(); @@ -1118,6 +1157,7 @@ class _QueueTabState extends ConsumerState { @override Widget build(BuildContext context) { _initializePageController(); + _applyDefaultLibraryViewOnTabVisible(); ref.listen(downloadQueueLookupProvider, (previous, next) { if (previous == null) return; @@ -1242,7 +1282,7 @@ class _QueueTabState extends ConsumerState { collectionState, totalTrackCount: switch (filterMode) { 'singles' => queueCounts.singleTrackCount, - 'albums' => 0, + 'albums' || 'playlists' => 0, _ => queueCounts.allTrackCount, }, totalAlbumCount: filterMode == 'albums' ? queueCounts.albumCount : null, @@ -1257,6 +1297,7 @@ class _QueueTabState extends ConsumerState { final currentTotalCount = switch (historyFilterMode) { 'albums' => queueCounts.albumCount, 'singles' => queueCounts.singleTrackCount, + 'playlists' => 0, _ => queueCounts.allTrackCount, }; final hasMoreLibrary = currentLoadedCount < currentTotalCount; @@ -1461,6 +1502,15 @@ class _QueueTabState extends ConsumerState { _animateToFilterPage(2); }, ), + const SizedBox(width: 8), + _FilterChip( + label: context.l10n.searchPlaylists, + count: collectionState.playlists.length, + isSelected: historyFilterMode == 'playlists', + onTap: () { + _animateToFilterPage(3); + }, + ), ], ), ); diff --git a/lib/screens/queue_tab_filter_widgets.dart b/lib/screens/queue_tab_filter_widgets.dart index 8f00dae3..f83cd98d 100644 --- a/lib/screens/queue_tab_filter_widgets.dart +++ b/lib/screens/queue_tab_filter_widgets.dart @@ -230,6 +230,19 @@ extension _QueueTabFilterWidgets on _QueueTabState { : const <_CollectionEntry>[]; final collectionCount = collectionEntries.length; + // Indexes into collectionState.playlists, name-filtered by the search + // query, for the 'playlists' view. + final playlistIndexes = []; + if (filterMode == 'playlists') { + final query = _searchQuery.trim().toLowerCase(); + for (var i = 0; i < collectionState.playlists.length; i++) { + if (query.isEmpty || + collectionState.playlists[i].name.toLowerCase().contains(query)) { + playlistIndexes.add(i); + } + } + } + Widget leadGridCell(int index) { if (index < activeDownloadIds.length) { final id = activeDownloadIds[index]; @@ -428,6 +441,49 @@ extension _QueueTabFilterWidgets on _QueueTabState { ), ), + if (filterMode == 'playlists' && playlistIndexes.isNotEmpty) ...[ + SliverToBoxAdapter( + child: _countHeaderRow( + context, + context.l10n.queuePlaylistCount(playlistIndexes.length), + [ + if (!_isPlaylistSelectionMode) + TextButton.icon( + onPressed: () => _showCreatePlaylistDialog(context), + icon: const Icon(Icons.add, size: 20), + label: Text(context.l10n.collectionCreatePlaylist), + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: _AnimatedLibrarySliverGrid( + maxCrossAxisExtent: _libraryAlbumGridExtent, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.72, + delegate: SliverChildBuilderDelegate((context, index) { + final playlistIndex = playlistIndexes[index]; + return KeyedSubtree( + key: ValueKey( + 'plgrid_${collectionState.playlists[playlistIndex].id}', + ), + child: _buildAllTabGridCollectionItem( + context: context, + colorScheme: colorScheme, + entry: _CollectionEntry.playlist(playlistIndex), + collectionState: collectionState, + ), + ); + }, childCount: playlistIndexes.length), + ), + ), + ], + if (filterMode == 'all') ...[ if (historyViewMode == 'grid') SliverPadding( @@ -666,6 +722,7 @@ extension _QueueTabFilterWidgets on _QueueTabState { (filterMode != 'albums' || (filteredGroupedAlbums.isEmpty && filteredGroupedLocalAlbums.isEmpty)) && + (filterMode != 'playlists' || playlistIndexes.isEmpty) && !showFilteringIndicator && !isPageLoading) SliverFillRemaining( @@ -693,7 +750,8 @@ extension _QueueTabFilterWidgets on _QueueTabState { totalTrackCount > 0 || (filterMode == 'albums' && (filteredGroupedAlbums.isNotEmpty || - filteredGroupedLocalAlbums.isNotEmpty))) + filteredGroupedLocalAlbums.isNotEmpty)) || + (filterMode == 'playlists' && playlistIndexes.isNotEmpty)) SliverToBoxAdapter( child: SizedBox(height: _isSelectionMode ? 100 : 16), ), @@ -770,6 +828,11 @@ extension _QueueTabFilterWidgets on _QueueTabState { subtitle = context.l10n.queueEmptySinglesSubtitle; icon = Icons.music_note; break; + case 'playlists': + message = context.l10n.collectionNoPlaylistsYet; + subtitle = context.l10n.queueEmptyPlaylistsSubtitle; + icon = Icons.queue_music; + break; default: message = context.l10n.queueEmptyHistory; subtitle = context.l10n.queueEmptyHistorySubtitle; diff --git a/lib/screens/queue_tab_helpers.dart b/lib/screens/queue_tab_helpers.dart index 6cd7facb..8a4bd5a8 100644 --- a/lib/screens/queue_tab_helpers.dart +++ b/lib/screens/queue_tab_helpers.dart @@ -273,6 +273,10 @@ final _queueLibraryPageProvider = FutureProvider.autoDispose ref.watch( localLibraryProvider.select((state) => state.loadedIndexVersion), ); + // Playlists render from libraryCollectionsProvider, not the DB. + if (request.filterMode == 'playlists') { + return const _QueueLibraryPageData(); + } final dbQuery = request.toDbQuery(); if (request.filterMode == 'albums') { final rows = await LibraryDatabase.instance.getQueueAlbumPage(dbQuery); diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index d84a8f03..cb8b2b83 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -337,6 +337,71 @@ class _LibrarySettingsPageState extends ConsumerState { ); } + String _getDefaultViewLabel(BuildContext context, String view) { + switch (view) { + case 'all': + return context.l10n.historyFilterAll; + case 'albums': + return context.l10n.historyFilterAlbums; + case 'singles': + return context.l10n.historyFilterSingles; + case 'playlists': + return context.l10n.searchPlaylists; + default: + return context.l10n.libraryDefaultViewLastUsed; + } + } + + void _showDefaultViewPicker(BuildContext context, String current) { + final colorScheme = Theme.of(context).colorScheme; + final options = [ + ('last', Icons.history, context.l10n.libraryDefaultViewLastUsed), + ('all', Icons.apps, context.l10n.historyFilterAll), + ('albums', Icons.album, context.l10n.historyFilterAlbums), + ('singles', Icons.music_note, context.l10n.historyFilterSingles), + ('playlists', Icons.queue_music, context.l10n.searchPlaylists), + ]; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 16), + child: Text( + context.l10n.libraryDefaultView, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + for (final (value, icon, label) in options) + _AutoScanOption( + icon: icon, + title: label, + selected: current == value, + colorScheme: colorScheme, + onTap: () { + ref + .read(settingsProvider.notifier) + .setDefaultLibraryView(value); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); @@ -369,6 +434,26 @@ class _LibrarySettingsPageState extends ConsumerState { ), ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.grid_view_rounded, + title: context.l10n.libraryDefaultView, + subtitle: _getDefaultViewLabel( + context, + settings.defaultLibraryView, + ), + onTap: () => _showDefaultViewPicker( + context, + settings.defaultLibraryView, + ), + showDivider: false, + ), + ], + ), + ), + SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.libraryScanSettings,