mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-25 01:04:11 +02:00
feat: add default search tab preference
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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를 탭하여 확장 기능에서 다시 전환하세요.';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 для возврата с расширения';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -35,6 +35,7 @@ class AppSettings {
|
||||
final bool useExtensionProviders;
|
||||
final List<String>? 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),
|
||||
|
||||
@@ -40,6 +40,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
|
||||
'useExtensionProviders': instance.useExtensionProviders,
|
||||
'downloadFallbackExtensionIds': instance.downloadFallbackExtensionIds,
|
||||
'searchProvider': instance.searchProvider,
|
||||
'defaultSearchTab': instance.defaultSearchTab,
|
||||
'homeFeedProvider': instance.homeFeedProvider,
|
||||
'separateSingles': instance.separateSingles,
|
||||
'singleFilenameFormat': instance.singleFilenameFormat,
|
||||
|
||||
@@ -18,6 +18,7 @@ final _log = AppLogger('SettingsProvider');
|
||||
|
||||
class SettingsNotifier extends Notifier<AppSettings> {
|
||||
static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$');
|
||||
static const Set<String> _searchTabValues = {'all', 'track', 'album'};
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
@@ -42,11 +43,15 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_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<AppSettings> {
|
||||
return 'US';
|
||||
}
|
||||
|
||||
String _normalizeDefaultSearchTab(String value) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (_searchTabValues.contains(normalized)) return normalized;
|
||||
return 'all';
|
||||
}
|
||||
|
||||
Future<void> _normalizeSongLinkRegionIfNeeded() async {
|
||||
final normalized = _normalizeSongLinkRegion(state.songLinkRegion);
|
||||
if (normalized == state.songLinkRegion) return;
|
||||
@@ -408,6 +419,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_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);
|
||||
|
||||
@@ -741,14 +741,16 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
String extensionId,
|
||||
String query, {
|
||||
Map<String, dynamic>? 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<TrackState> {
|
||||
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<TrackState> {
|
||||
error: e.toString(),
|
||||
hasSearchText: state.hasSearchText,
|
||||
isShowingRecentAccess: state.isShowingRecentAccess,
|
||||
selectedSearchFilter: currentFilter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,6 +449,61 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
];
|
||||
}
|
||||
|
||||
String? _sanitizeSearchFilterForProvider(
|
||||
String? filter,
|
||||
String? currentSearchProvider,
|
||||
List<Extension> 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<Extension> extensions,
|
||||
) {
|
||||
final preferred = switch (preferredSearchTab) {
|
||||
'track' => 'track',
|
||||
'album' => 'album',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return _sanitizeSearchFilterForProvider(
|
||||
preferred,
|
||||
currentSearchProvider,
|
||||
extensions,
|
||||
);
|
||||
}
|
||||
|
||||
_SearchResultBuckets _getSearchResultBuckets(List<Track> tracks) {
|
||||
final cached = _searchBucketsCache;
|
||||
if (cached != null && identical(tracks, _searchBucketsSourceTracks)) {
|
||||
@@ -601,7 +656,21 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
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<HomeTab>
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user