From 3735aaf3bdc6e33e4facc5f7a902f6bc5d943d49 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 11 Apr 2026 16:42:29 +0700 Subject: [PATCH] feat: add default search tab preference --- lib/l10n/app_localizations.dart | 12 +++ lib/l10n/app_localizations_de.dart | 7 ++ lib/l10n/app_localizations_en.dart | 7 ++ lib/l10n/app_localizations_es.dart | 7 ++ lib/l10n/app_localizations_fr.dart | 7 ++ lib/l10n/app_localizations_hi.dart | 7 ++ lib/l10n/app_localizations_id.dart | 7 ++ lib/l10n/app_localizations_ja.dart | 7 ++ lib/l10n/app_localizations_ko.dart | 7 ++ lib/l10n/app_localizations_nl.dart | 7 ++ lib/l10n/app_localizations_pt.dart | 7 ++ lib/l10n/app_localizations_ru.dart | 7 ++ lib/l10n/app_localizations_tr.dart | 7 ++ lib/l10n/app_localizations_zh.dart | 7 ++ lib/l10n/arb/app_en.arb | 8 ++ lib/l10n/arb/app_id.arb | 8 ++ lib/models/settings.dart | 4 + lib/models/settings.g.dart | 2 + lib/providers/settings_provider.dart | 16 ++++ lib/providers/track_provider.dart | 7 +- lib/screens/home_tab.dart | 78 ++++++++++++++++++- .../settings/options_settings_page.dart | 72 ++++++++++++++++- 22 files changed, 293 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 725e2ac1..e6871f7f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -352,6 +352,18 @@ abstract class AppLocalizations { /// **'Using extension: {extensionName}'** String optionsUsingExtension(String extensionName); + /// Title for the preferred default search tab setting + /// + /// In en, this message translates to: + /// **'Default Search Tab'** + String get optionsDefaultSearchTab; + + /// Subtitle for the preferred default search tab setting + /// + /// In en, this message translates to: + /// **'Choose which tab opens first for new search results.'** + String get optionsDefaultSearchTabSubtitle; + /// Hint to switch back to built-in providers /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 12e4e4e1..88f2981c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -129,6 +129,13 @@ class AppLocalizationsDe extends AppLocalizations { return 'Erweiterung verwenden: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tippe auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1505bba9..232da15b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -127,6 +127,13 @@ class AppLocalizationsEn extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c9adcee3..c813f73b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -127,6 +127,13 @@ class AppLocalizationsEs extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 33b30681..95aa50b3 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -128,6 +128,13 @@ class AppLocalizationsFr extends AppLocalizations { return 'Utilisation de l\'extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Appuyez sur Deezer ou Spotify pour revenir à l\'extension'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index c6a4e581..d542fc63 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -127,6 +127,13 @@ class AppLocalizationsHi extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index ccd90596..9ff27578 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -129,6 +129,13 @@ class AppLocalizationsId extends AppLocalizations { return 'Menggunakan ekstensi: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Tab Pencarian Default'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Pilih tab yang dibuka lebih dulu untuk hasil pencarian baru.'; + @override String get optionsSwitchBack => 'Ketuk Deezer atau Spotify untuk beralih dari ekstensi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 7114d13f..baa3777e 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -127,6 +127,13 @@ class AppLocalizationsJa extends AppLocalizations { return '拡張の使用: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 5d112492..b7f01002 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -125,6 +125,13 @@ class AppLocalizationsKo extends AppLocalizations { return '확장 기능을 사용: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Deezer 또는 Spotify를 탭하여 확장 기능에서 다시 전환하세요.'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e56da9c5..d89b3158 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -127,6 +127,13 @@ class AppLocalizationsNl extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 83c56082..cd81ead6 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -127,6 +127,13 @@ class AppLocalizationsPt extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 913e2439..73c69db7 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -129,6 +129,13 @@ class AppLocalizationsRu extends AppLocalizations { return 'Используется расширение: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Нажмите Deezer или Spotify для возврата с расширения'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index b747d28c..df2536d4 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -129,6 +129,13 @@ class AppLocalizationsTr extends AppLocalizations { return 'Kullanılan eklenti: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Dahili kaynaklara dönmek için Deezer veya Spotify\'a tıkla'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4ada22db..f6230541 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -127,6 +127,13 @@ class AppLocalizationsZh extends AppLocalizations { return 'Using extension: $extensionName'; } + @override + String get optionsDefaultSearchTab => 'Default Search Tab'; + + @override + String get optionsDefaultSearchTabSubtitle => + 'Choose which tab opens first for new search results.'; + @override String get optionsSwitchBack => 'Tap Deezer or Spotify to switch back from extension'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e7e07650..e3f4a6d0 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -158,6 +158,14 @@ } } }, + "optionsDefaultSearchTab": "Default Search Tab", + "@optionsDefaultSearchTab": { + "description": "Title for the preferred default search tab setting" + }, + "optionsDefaultSearchTabSubtitle": "Choose which tab opens first for new search results.", + "@optionsDefaultSearchTabSubtitle": { + "description": "Subtitle for the preferred default search tab setting" + }, "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 616e401b..e54cd15f 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -150,6 +150,14 @@ } } }, + "optionsDefaultSearchTab": "Tab Pencarian Default", + "@optionsDefaultSearchTab": { + "description": "Title for the preferred default search tab setting" + }, + "optionsDefaultSearchTabSubtitle": "Pilih tab yang dibuka lebih dulu untuk hasil pencarian baru.", + "@optionsDefaultSearchTabSubtitle": { + "description": "Subtitle for the preferred default search tab setting" + }, "optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 10c8f05d..147c2c20 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -35,6 +35,7 @@ class AppSettings { final bool useExtensionProviders; final List? downloadFallbackExtensionIds; final String? searchProvider; + final String defaultSearchTab; final String? homeFeedProvider; final bool separateSingles; final String singleFilenameFormat; @@ -111,6 +112,7 @@ class AppSettings { this.useExtensionProviders = true, this.downloadFallbackExtensionIds, this.searchProvider, + this.defaultSearchTab = 'all', this.homeFeedProvider, this.separateSingles = false, this.singleFilenameFormat = '{title} - {artist}', @@ -176,6 +178,7 @@ class AppSettings { bool clearDownloadFallbackExtensionIds = false, String? searchProvider, bool clearSearchProvider = false, + String? defaultSearchTab, String? homeFeedProvider, bool clearHomeFeedProvider = false, bool? separateSingles, @@ -242,6 +245,7 @@ class AppSettings { searchProvider: clearSearchProvider ? null : (searchProvider ?? this.searchProvider), + defaultSearchTab: defaultSearchTab ?? this.defaultSearchTab, homeFeedProvider: clearHomeFeedProvider ? null : (homeFeedProvider ?? this.homeFeedProvider), diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index efa2cb6d..10e40855 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -40,6 +40,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( ?.map((e) => e as String) .toList(), searchProvider: json['searchProvider'] as String?, + defaultSearchTab: json['defaultSearchTab'] as String? ?? 'all', homeFeedProvider: json['homeFeedProvider'] as String?, separateSingles: json['separateSingles'] as bool? ?? false, singleFilenameFormat: @@ -111,6 +112,7 @@ Map _$AppSettingsToJson( 'useExtensionProviders': instance.useExtensionProviders, 'downloadFallbackExtensionIds': instance.downloadFallbackExtensionIds, 'searchProvider': instance.searchProvider, + 'defaultSearchTab': instance.defaultSearchTab, 'homeFeedProvider': instance.homeFeedProvider, 'separateSingles': instance.separateSingles, 'singleFilenameFormat': instance.singleFilenameFormat, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 14df2d67..01bc97e4 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -18,6 +18,7 @@ final _log = AppLogger('SettingsProvider'); class SettingsNotifier extends Notifier { static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$'); + static const Set _searchTabValues = {'all', 'track', 'album'}; final Future _prefs = SharedPreferences.getInstance(); final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); @@ -42,11 +43,15 @@ class SettingsNotifier extends Notifier { _sanitizeDownloadFallbackExtensionIds( loaded.downloadFallbackExtensionIds, ); + final sanitizedDefaultSearchTab = _normalizeDefaultSearchTab( + loaded.defaultSearchTab, + ); state = loaded.copyWith( downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds, clearDownloadFallbackExtensionIds: loaded.downloadFallbackExtensionIds != null && sanitizedDownloadFallbackExtensionIds == null, + defaultSearchTab: sanitizedDefaultSearchTab, ); await _runMigrations(prefs); @@ -187,6 +192,12 @@ class SettingsNotifier extends Notifier { return 'US'; } + String _normalizeDefaultSearchTab(String value) { + final normalized = value.trim().toLowerCase(); + if (_searchTabValues.contains(normalized)) return normalized; + return 'all'; + } + Future _normalizeSongLinkRegionIfNeeded() async { final normalized = _normalizeSongLinkRegion(state.songLinkRegion); if (normalized == state.songLinkRegion) return; @@ -408,6 +419,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setDefaultSearchTab(String tab) { + state = state.copyWith(defaultSearchTab: _normalizeDefaultSearchTab(tab)); + _saveSettings(); + } + void setHomeFeedProvider(String? provider) { if (provider == null || provider.isEmpty) { state = state.copyWith(clearHomeFeedProvider: true); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index b1786dca..726c8ca6 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -741,14 +741,16 @@ class TrackNotifier extends Notifier { String extensionId, String query, { Map? options, + String? selectedFilter, }) async { final requestId = ++_currentRequestId; + final currentFilter = selectedFilter ?? state.selectedSearchFilter; state = TrackState( isLoading: true, hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, - selectedSearchFilter: state.selectedSearchFilter, + selectedSearchFilter: currentFilter, ); try { @@ -788,7 +790,7 @@ class TrackNotifier extends Notifier { hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, searchExtensionId: extensionId, - selectedSearchFilter: state.selectedSearchFilter, + selectedSearchFilter: currentFilter, ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; @@ -798,6 +800,7 @@ class TrackNotifier extends Notifier { error: e.toString(), hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, + selectedSearchFilter: currentFilter, ); } } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 439cdadd..af7b0d43 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -449,6 +449,61 @@ class _HomeTabState extends ConsumerState ]; } + String? _sanitizeSearchFilterForProvider( + String? filter, + String? currentSearchProvider, + List extensions, + ) { + if (filter == null || filter.isEmpty) { + return null; + } + + if (currentSearchProvider == null || + currentSearchProvider.isEmpty || + _builtInSearchProviders.contains(currentSearchProvider)) { + switch (filter) { + case 'track': + case 'artist': + case 'album': + case 'playlist': + return filter; + default: + return null; + } + } + + final extension = extensions + .where((e) => e.id == currentSearchProvider && e.enabled) + .firstOrNull; + final filters = extension?.searchBehavior?.filters; + if (filters == null || filters.isEmpty) { + return null; + } + + final match = filters + .where((candidate) => candidate.id == filter) + .firstOrNull; + return match?.id; + } + + String? _preferredSearchFilter( + String preferredSearchTab, + String? currentSearchProvider, + List extensions, + ) { + final preferred = switch (preferredSearchTab) { + 'track' => 'track', + 'album' => 'album', + _ => null, + }; + + return _sanitizeSearchFilterForProvider( + preferred, + currentSearchProvider, + extensions, + ); + } + _SearchResultBuckets _getSearchResultBuckets(List tracks) { final cached = _searchBucketsCache; if (cached != null && identical(tracks, _searchBucketsSourceTracks)) { @@ -601,7 +656,21 @@ class _HomeTabState extends ConsumerState final extState = ref.read(extensionProvider); final searchProvider = settings.searchProvider; final selectedFilter = - filterOverride ?? ref.read(trackProvider).selectedSearchFilter; + _sanitizeSearchFilterForProvider( + filterOverride, + searchProvider, + extState.extensions, + ) ?? + _sanitizeSearchFilterForProvider( + ref.read(trackProvider).selectedSearchFilter, + searchProvider, + extState.extensions, + ) ?? + _preferredSearchFilter( + settings.defaultSearchTab, + searchProvider, + extState.extensions, + ); final searchKey = '${searchProvider ?? 'default'}:$query:${selectedFilter ?? 'all'}'; @@ -627,7 +696,12 @@ class _HomeTabState extends ConsumerState } await ref .read(trackProvider.notifier) - .customSearch(searchProvider, query, options: options); + .customSearch( + searchProvider, + query, + options: options, + selectedFilter: selectedFilter, + ); } else if (isBuiltInProvider) { await ref .read(trackProvider.notifier) diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 940fef74..8ead2d5b 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -70,7 +70,12 @@ class OptionsSettingsPage extends ConsumerWidget { ), ), SliverToBoxAdapter( - child: SettingsGroup(children: [const _MetadataSourceSelector()]), + child: SettingsGroup( + children: const [ + _MetadataSourceSelector(), + _DefaultSearchTabSelector(), + ], + ), ), SliverToBoxAdapter( @@ -826,6 +831,71 @@ class _MetadataSourceSelector extends ConsumerWidget { } } +class _DefaultSearchTabSelector extends ConsumerWidget { + const _DefaultSearchTabSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final selectedTab = ref.watch( + settingsProvider.select((s) => s.defaultSearchTab), + ); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.optionsDefaultSearchTab, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text( + context.l10n.optionsDefaultSearchTabSubtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SourceChip( + icon: Icons.dashboard_outlined, + label: context.l10n.historyFilterAll, + isSelected: selectedTab == 'all', + onTap: () => ref + .read(settingsProvider.notifier) + .setDefaultSearchTab('all'), + ), + _SourceChip( + icon: Icons.music_note, + label: context.l10n.searchSongs, + isSelected: selectedTab == 'track', + onTap: () => ref + .read(settingsProvider.notifier) + .setDefaultSearchTab('track'), + ), + _SourceChip( + icon: Icons.album, + label: context.l10n.searchAlbums, + isSelected: selectedTab == 'album', + onTap: () => ref + .read(settingsProvider.notifier) + .setDefaultSearchTab('album'), + ), + ], + ), + ], + ), + ); + } +} + class _SourceChip extends StatelessWidget { final IconData icon; final String label;