diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8be00df1..7b6273a4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1432,6 +1432,66 @@ abstract class AppLocalizations { /// **'Playlists'** String get searchPlaylists; + /// Bottom sheet title for search sort options + /// + /// In en, this message translates to: + /// **'Sort Results'** + String get searchSortTitle; + + /// Sort option - default API order + /// + /// In en, this message translates to: + /// **'Default'** + String get searchSortDefault; + + /// Sort option - title ascending + /// + /// In en, this message translates to: + /// **'Title (A-Z)'** + String get searchSortTitleAZ; + + /// Sort option - title descending + /// + /// In en, this message translates to: + /// **'Title (Z-A)'** + String get searchSortTitleZA; + + /// Sort option - artist ascending + /// + /// In en, this message translates to: + /// **'Artist (A-Z)'** + String get searchSortArtistAZ; + + /// Sort option - artist descending + /// + /// In en, this message translates to: + /// **'Artist (Z-A)'** + String get searchSortArtistZA; + + /// Sort option - shortest duration first + /// + /// In en, this message translates to: + /// **'Duration (Shortest)'** + String get searchSortDurationShort; + + /// Sort option - longest duration first + /// + /// In en, this message translates to: + /// **'Duration (Longest)'** + String get searchSortDurationLong; + + /// Sort option - oldest release first + /// + /// In en, this message translates to: + /// **'Release Date (Oldest)'** + String get searchSortDateOldest; + + /// Sort option - newest release first + /// + /// In en, this message translates to: + /// **'Release Date (Newest)'** + String get searchSortDateNewest; + /// Tooltip - play button /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2db9eedb..f64670cd 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -772,6 +772,36 @@ class AppLocalizationsDe extends AppLocalizations { @override String get searchPlaylists => 'Playlisten'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Abspielen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 77d56ebc..e1e437db 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -759,6 +759,36 @@ class AppLocalizationsEn extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 165d9242..dbaa5d7f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -759,6 +759,36 @@ class AppLocalizationsEs extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f4c4251b..9f0a9b86 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -761,6 +761,36 @@ class AppLocalizationsFr extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 15c12b13..a63fe541 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -759,6 +759,36 @@ class AppLocalizationsHi extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 2d8ae360..67f85972 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -762,6 +762,36 @@ class AppLocalizationsId extends AppLocalizations { @override String get searchPlaylists => 'Playlist'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Putar'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 508a09e1..c759276c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -754,6 +754,36 @@ class AppLocalizationsJa extends AppLocalizations { @override String get searchPlaylists => 'プレイリスト'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => '再生'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index a7785ece..6d1993e2 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -741,6 +741,36 @@ class AppLocalizationsKo extends AppLocalizations { @override String get searchPlaylists => '재생목록들'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => '재생'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 213c2dc9..054c9667 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -759,6 +759,36 @@ class AppLocalizationsNl extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 1456e984..1c9aba43 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -759,6 +759,36 @@ class AppLocalizationsPt extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 592f73a9..8348e0b7 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -773,6 +773,36 @@ class AppLocalizationsRu extends AppLocalizations { @override String get searchPlaylists => 'Плейлисты'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Воспроизвести'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index fe1dbad7..70180433 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -764,6 +764,36 @@ class AppLocalizationsTr extends AppLocalizations { @override String get searchPlaylists => 'Çalma Listeleri'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Oynat'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index df0493e1..e52acfad 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -759,6 +759,36 @@ class AppLocalizationsZh extends AppLocalizations { @override String get searchPlaylists => 'Playlists'; + @override + String get searchSortTitle => 'Sort Results'; + + @override + String get searchSortDefault => 'Default'; + + @override + String get searchSortTitleAZ => 'Title (A-Z)'; + + @override + String get searchSortTitleZA => 'Title (Z-A)'; + + @override + String get searchSortArtistAZ => 'Artist (A-Z)'; + + @override + String get searchSortArtistZA => 'Artist (Z-A)'; + + @override + String get searchSortDurationShort => 'Duration (Shortest)'; + + @override + String get searchSortDurationLong => 'Duration (Longest)'; + + @override + String get searchSortDateOldest => 'Release Date (Oldest)'; + + @override + String get searchSortDateNewest => 'Release Date (Newest)'; + @override String get tooltipPlay => 'Play'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 807f9427..ed71d48f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -999,6 +999,46 @@ "@searchPlaylists": { "description": "Search result category - playlists" }, + "searchSortTitle": "Sort Results", + "@searchSortTitle": { + "description": "Bottom sheet title for search sort options" + }, + "searchSortDefault": "Default", + "@searchSortDefault": { + "description": "Sort option - default API order" + }, + "searchSortTitleAZ": "Title (A-Z)", + "@searchSortTitleAZ": { + "description": "Sort option - title ascending" + }, + "searchSortTitleZA": "Title (Z-A)", + "@searchSortTitleZA": { + "description": "Sort option - title descending" + }, + "searchSortArtistAZ": "Artist (A-Z)", + "@searchSortArtistAZ": { + "description": "Sort option - artist ascending" + }, + "searchSortArtistZA": "Artist (Z-A)", + "@searchSortArtistZA": { + "description": "Sort option - artist descending" + }, + "searchSortDurationShort": "Duration (Shortest)", + "@searchSortDurationShort": { + "description": "Sort option - shortest duration first" + }, + "searchSortDurationLong": "Duration (Longest)", + "@searchSortDurationLong": { + "description": "Sort option - longest duration first" + }, + "searchSortDateOldest": "Release Date (Oldest)", + "@searchSortDateOldest": { + "description": "Sort option - oldest release first" + }, + "searchSortDateNewest": "Release Date (Newest)", + "@searchSortDateNewest": { + "description": "Sort option - newest release first" + }, "tooltipPlay": "Play", "@tooltipPlay": { "description": "Tooltip - play button" diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index b135b86c..65177c94 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -83,6 +83,18 @@ class _SearchResultBuckets { }); } +enum _SearchSortOption { + defaultOrder, + titleAsc, + titleDesc, + artistAsc, + artistDesc, + durationAsc, + durationDesc, + dateAsc, + dateDesc, +} + const _homeHistoryPreviewLimit = 48; class _HomeHistoryPreview { @@ -244,6 +256,7 @@ class _HomeTabState extends ConsumerState Map? _thumbnailSizesCache; List? _searchBucketsSourceTracks; _SearchResultBuckets? _searchBucketsCache; + _SearchSortOption _searchSortOption = _SearchSortOption.defaultOrder; double _responsiveScale({ required BuildContext context, @@ -564,6 +577,7 @@ class _HomeTabState extends ConsumerState '${searchProvider ?? 'default'}:$query:${selectedFilter ?? 'all'}'; if (_lastSearchQuery == searchKey) return; _lastSearchQuery = searchKey; + _searchSortOption = _SearchSortOption.defaultOrder; final isBuiltInProvider = searchProvider != null && @@ -2399,6 +2413,168 @@ class _HomeTabState extends ConsumerState ); } + // ── Search result sorting ────────────────────────────────────────────── + + String _sortOptionLabel(_SearchSortOption option) { + switch (option) { + case _SearchSortOption.defaultOrder: + return context.l10n.searchSortDefault; + case _SearchSortOption.titleAsc: + return context.l10n.searchSortTitleAZ; + case _SearchSortOption.titleDesc: + return context.l10n.searchSortTitleZA; + case _SearchSortOption.artistAsc: + return context.l10n.searchSortArtistAZ; + case _SearchSortOption.artistDesc: + return context.l10n.searchSortArtistZA; + case _SearchSortOption.durationAsc: + return context.l10n.searchSortDurationShort; + case _SearchSortOption.durationDesc: + return context.l10n.searchSortDurationLong; + case _SearchSortOption.dateAsc: + return context.l10n.searchSortDateOldest; + case _SearchSortOption.dateDesc: + return context.l10n.searchSortDateNewest; + } + } + + void _showSortOptions(ColorScheme colorScheme) { + var tempSort = _searchSortOption; + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: colorScheme.surfaceContainerLow, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (ctx) => StatefulBuilder( + builder: (ctx, setSheetState) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + children: [ + Text( + context.l10n.searchSortTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + TextButton( + onPressed: () => setSheetState( + () => tempSort = _SearchSortOption.defaultOrder, + ), + child: Text(context.l10n.libraryFilterReset), + ), + ], + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: _SearchSortOption.values.map((option) { + return FilterChip( + label: Text(_sortOptionLabel(option)), + selected: tempSort == option, + showCheckmark: false, + onSelected: (_) => + setSheetState(() => tempSort = option), + ); + }).toList(), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + Navigator.pop(ctx); + if (_searchSortOption != tempSort) { + setState(() { + _searchSortOption = tempSort; + }); + } + }, + child: Text(context.l10n.libraryFilterApply), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + List _applySortToList( + List items, + String Function(T) getName, + String Function(T) getArtist, + int Function(T) getDuration, + String? Function(T) getDate, + ) { + if (_searchSortOption == _SearchSortOption.defaultOrder) return items; + final sorted = List.of(items); + switch (_searchSortOption) { + case _SearchSortOption.defaultOrder: + break; + case _SearchSortOption.titleAsc: + sorted.sort( + (a, b) => + getName(a).toLowerCase().compareTo(getName(b).toLowerCase()), + ); + case _SearchSortOption.titleDesc: + sorted.sort( + (a, b) => + getName(b).toLowerCase().compareTo(getName(a).toLowerCase()), + ); + case _SearchSortOption.artistAsc: + sorted.sort( + (a, b) => + getArtist(a).toLowerCase().compareTo(getArtist(b).toLowerCase()), + ); + case _SearchSortOption.artistDesc: + sorted.sort( + (a, b) => + getArtist(b).toLowerCase().compareTo(getArtist(a).toLowerCase()), + ); + case _SearchSortOption.durationAsc: + sorted.sort((a, b) => getDuration(a).compareTo(getDuration(b))); + case _SearchSortOption.durationDesc: + sorted.sort((a, b) => getDuration(b).compareTo(getDuration(a))); + case _SearchSortOption.dateAsc: + sorted.sort((a, b) { + final da = getDate(a) ?? ''; + final db = getDate(b) ?? ''; + return da.compareTo(db); + }); + case _SearchSortOption.dateDesc: + sorted.sort((a, b) { + final da = getDate(a) ?? ''; + final db = getDate(b) ?? ''; + return db.compareTo(da); + }); + } + return sorted; + } + List _buildSearchResults({ required List tracks, required List? searchArtists, @@ -2423,6 +2599,61 @@ class _HomeTabState extends ConsumerState final playlistItems = buckets.playlistItems; final artistItems = buckets.artistItems; + // Apply sorting to each list. + final sortedArtists = searchArtists != null && searchArtists.isNotEmpty + ? _applySortToList( + searchArtists, + (a) => a.name, + (a) => a.name, + (a) => 0, + (a) => null, + ) + : searchArtists; + + final sortedAlbums = searchAlbums != null && searchAlbums.isNotEmpty + ? _applySortToList( + searchAlbums, + (a) => a.name, + (a) => a.artists, + (a) => 0, + (a) => a.releaseDate, + ) + : searchAlbums; + + final sortedPlaylists = + searchPlaylists != null && searchPlaylists.isNotEmpty + ? _applySortToList( + searchPlaylists, + (p) => p.name, + (p) => p.owner, + (p) => 0, + (p) => null, + ) + : searchPlaylists; + + // For tracks we need paired sorting (track + original index). + List sortedTracks; + List sortedTrackIndexes; + if (realTracks.isNotEmpty && + _searchSortOption != _SearchSortOption.defaultOrder) { + final paired = List.generate( + realTracks.length, + (i) => (realTracks[i], realTrackIndexes[i]), + ); + final sortedPairs = _applySortToList<(Track, int)>( + paired, + (p) => p.$1.name, + (p) => p.$1.artistName, + (p) => p.$1.duration, + (p) => p.$1.releaseDate, + ); + sortedTracks = sortedPairs.map((p) => p.$1).toList(); + sortedTrackIndexes = sortedPairs.map((p) => p.$2).toList(); + } else { + sortedTracks = realTracks; + sortedTrackIndexes = realTrackIndexes; + } + final slivers = [ if (error != null) SliverToBoxAdapter( @@ -2440,24 +2671,29 @@ class _HomeTabState extends ConsumerState ), ]; - if (searchArtists != null && searchArtists.isNotEmpty) { + // Track whether the sort button has been shown yet (show on first section). + bool sortButtonShown = false; + + if (sortedArtists != null && sortedArtists.isNotEmpty) { slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchArtists, - itemCount: searchArtists.length, + itemCount: sortedArtists.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _SearchArtistItemWidget( - key: ValueKey('search-artist-${searchArtists[index].id}'), - artist: searchArtists[index], + key: ValueKey('search-artist-${sortedArtists[index].id}'), + artist: sortedArtists[index], showDivider: showDivider, onTap: () => _navigateToArtist( - searchArtists[index].id, - searchArtists[index].name, - searchArtists[index].imageUrl, + sortedArtists[index].id, + sortedArtists[index].name, + sortedArtists[index].imageUrl, ), ), ), ); + sortButtonShown = true; } if (artistItems.isNotEmpty) { @@ -2466,6 +2702,7 @@ class _HomeTabState extends ConsumerState title: context.l10n.searchArtists, itemCount: artistItems.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _CollectionItemWidget( key: ValueKey('artist-${artistItems[index].id}'), item: artistItems[index], @@ -2474,22 +2711,25 @@ class _HomeTabState extends ConsumerState ), ), ); + sortButtonShown = true; } - if (searchAlbums != null && searchAlbums.isNotEmpty) { + if (sortedAlbums != null && sortedAlbums.isNotEmpty) { slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchAlbums, - itemCount: searchAlbums.length, + itemCount: sortedAlbums.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _SearchAlbumItemWidget( - key: ValueKey('search-album-${searchAlbums[index].id}'), - album: searchAlbums[index], + key: ValueKey('search-album-${sortedAlbums[index].id}'), + album: sortedAlbums[index], showDivider: showDivider, - onTap: () => _navigateToSearchAlbum(searchAlbums[index]), + onTap: () => _navigateToSearchAlbum(sortedAlbums[index]), ), ), ); + sortButtonShown = true; } if (albumItems.isNotEmpty) { @@ -2498,6 +2738,7 @@ class _HomeTabState extends ConsumerState title: context.l10n.searchAlbums, itemCount: albumItems.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _CollectionItemWidget( key: ValueKey('album-${albumItems[index].id}'), item: albumItems[index], @@ -2506,22 +2747,25 @@ class _HomeTabState extends ConsumerState ), ), ); + sortButtonShown = true; } - if (searchPlaylists != null && searchPlaylists.isNotEmpty) { + if (sortedPlaylists != null && sortedPlaylists.isNotEmpty) { slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchPlaylists, - itemCount: searchPlaylists.length, + itemCount: sortedPlaylists.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _SearchPlaylistItemWidget( - key: ValueKey('search-playlist-${searchPlaylists[index].id}'), - playlist: searchPlaylists[index], + key: ValueKey('search-playlist-${sortedPlaylists[index].id}'), + playlist: sortedPlaylists[index], showDivider: showDivider, - onTap: () => _navigateToSearchPlaylist(searchPlaylists[index]), + onTap: () => _navigateToSearchPlaylist(sortedPlaylists[index]), ), ), ); + sortButtonShown = true; } if (playlistItems.isNotEmpty) { @@ -2530,6 +2774,7 @@ class _HomeTabState extends ConsumerState title: context.l10n.searchPlaylists, itemCount: playlistItems.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _CollectionItemWidget( key: ValueKey('playlist-${playlistItems[index].id}'), item: playlistItems[index], @@ -2538,20 +2783,22 @@ class _HomeTabState extends ConsumerState ), ), ); + sortButtonShown = true; } - if (realTracks.isNotEmpty) { + if (sortedTracks.isNotEmpty) { slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchSongs, - itemCount: realTracks.length, + itemCount: sortedTracks.length, colorScheme: colorScheme, + showSortButton: !sortButtonShown, itemBuilder: (index, showDivider) => _TrackItemWithStatus( - key: ValueKey(realTracks[index].id), - track: realTracks[index], - index: realTrackIndexes[index], + key: ValueKey(sortedTracks[index].id), + track: sortedTracks[index], + index: sortedTrackIndexes[index], showDivider: showDivider, - onDownload: () => _downloadTrack(realTrackIndexes[index]), + onDownload: () => _downloadTrack(sortedTrackIndexes[index]), searchExtensionId: searchExtensionId, showLocalLibraryIndicator: showLocalLibraryIndicator, thumbnailSizesByExtensionId: thumbnailSizesByExtensionId, @@ -2569,6 +2816,7 @@ class _HomeTabState extends ConsumerState required int itemCount, required ColorScheme colorScheme, required Widget Function(int index, bool showDivider) itemBuilder, + bool showSortButton = false, }) { final sectionColor = Theme.of(context).brightness == Brightness.dark ? Color.alphaBlend( @@ -2580,12 +2828,47 @@ class _HomeTabState extends ConsumerState return [ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text( - title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + if (showSortButton) + SizedBox( + height: 32, + child: TextButton.icon( + onPressed: () => _showSortOptions(colorScheme), + icon: Icon( + Icons.swap_vert, + size: 18, + color: _searchSortOption != _SearchSortOption.defaultOrder + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + label: Text( + _searchSortOption != _SearchSortOption.defaultOrder + ? _sortOptionLabel(_searchSortOption) + : context.l10n.libraryFilterSort, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: + _searchSortOption != _SearchSortOption.defaultOrder + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + visualDensity: VisualDensity.compact, + ), + ), + ), + ], ), ), ),