mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => '다운로드 기록이 없음';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Нет истории скачиваний';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Немає історії завантажень';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,6 +33,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
|
||||
instance.filterContributingArtistsInAlbumArtist,
|
||||
'historyViewMode': instance.historyViewMode,
|
||||
'historyFilterMode': instance.historyFilterMode,
|
||||
'defaultLibraryView': instance.defaultLibraryView,
|
||||
'askQualityBeforeDownload': instance.askQualityBeforeDownload,
|
||||
'enableLogging': instance.enableLogging,
|
||||
'useExtensionProviders': instance.useExtensionProviders,
|
||||
|
||||
@@ -96,6 +96,13 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
'album',
|
||||
'playlist',
|
||||
};
|
||||
static const Set<String> _libraryViewValues = {
|
||||
'last',
|
||||
'all',
|
||||
'albums',
|
||||
'singles',
|
||||
'playlists',
|
||||
};
|
||||
static const Set<String> _extensionVerificationBrowserModeValues = {
|
||||
'external_first',
|
||||
'in_app_first',
|
||||
@@ -149,6 +156,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
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<AppSettings> {
|
||||
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<AppSettings> {
|
||||
_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);
|
||||
|
||||
@@ -238,8 +238,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
double _playlistSelectionOverlayBottomPadding = 0;
|
||||
|
||||
PageController? _filterPageController;
|
||||
final List<String> _filterModes = ['all', 'albums', 'singles'];
|
||||
final List<String> _filterModes = ['all', 'albums', 'singles', 'playlists'];
|
||||
bool _isPageControllerInitialized = false;
|
||||
bool _wasTabVisible = false;
|
||||
static const List<String> _months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
@@ -327,11 +328,49 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
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<QueueTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initializePageController();
|
||||
_applyDefaultLibraryViewOnTabVisible();
|
||||
|
||||
ref.listen(downloadQueueLookupProvider, (previous, next) {
|
||||
if (previous == null) return;
|
||||
@@ -1242,7 +1282,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
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<QueueTab> {
|
||||
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<QueueTab> {
|
||||
_animateToFilterPage(2);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
label: context.l10n.searchPlaylists,
|
||||
count: collectionState.playlists.length,
|
||||
isSelected: historyFilterMode == 'playlists',
|
||||
onTap: () {
|
||||
_animateToFilterPage(3);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 = <int>[];
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -337,6 +337,71 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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<void>(
|
||||
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<LibrarySettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user