mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
feat(settings): search nested options
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
/// A control or submenu that can be discovered from the root Settings search.
|
||||
///
|
||||
/// The catalog contains only presentation metadata. The owning settings page
|
||||
/// remains responsible for reading and changing the actual setting value.
|
||||
class SettingsSearchEntry {
|
||||
const SettingsSearchEntry({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.keywords = const [],
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final List<String> keywords;
|
||||
|
||||
bool matches(SettingsSearchQuery query) =>
|
||||
query.matches([title, ?subtitle, ...keywords]);
|
||||
}
|
||||
|
||||
/// Normalizes the query once per keystroke, rather than once for every entry.
|
||||
class SettingsSearchQuery {
|
||||
SettingsSearchQuery(String value)
|
||||
: terms = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(RegExp(r'\s+'))
|
||||
.where((term) => term.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
final List<String> terms;
|
||||
|
||||
bool get isEmpty => terms.isEmpty;
|
||||
|
||||
bool matches(Iterable<String> values) {
|
||||
if (isEmpty) return true;
|
||||
final haystack = values.join(' ').toLowerCase();
|
||||
return terms.every(haystack.contains);
|
||||
}
|
||||
}
|
||||
|
||||
/// Localized index of controls found inside every first-level settings page.
|
||||
class SettingsSearchCatalog {
|
||||
SettingsSearchCatalog(AppLocalizations l10n)
|
||||
: extensions = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.low_priority,
|
||||
title: l10n.extensionsDownloadPriority,
|
||||
subtitle: l10n.extensionsDownloadPrioritySubtitle,
|
||||
keywords: const ['provider order', 'service order'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.alt_route,
|
||||
title: l10n.extensionsFallbackTitle,
|
||||
subtitle: l10n.extensionsFallbackSubtitle,
|
||||
keywords: const ['provider fallback'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.manage_search,
|
||||
title: l10n.extensionsMetadataPriority,
|
||||
subtitle: l10n.extensionsMetadataPrioritySubtitle,
|
||||
keywords: const ['metadata provider order'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.search,
|
||||
title: l10n.extensionsSearchProvider,
|
||||
subtitle: l10n.extensionsSearchProviderDescription,
|
||||
keywords: const ['home search source'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.explore_outlined,
|
||||
title: l10n.extensionsHomeFeedProvider,
|
||||
subtitle: l10n.extensionsHomeFeedDescription,
|
||||
keywords: const ['home provider', 'feed source'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.add,
|
||||
title: l10n.extensionsInstallButton,
|
||||
keywords: const ['add plugin', 'import extension'],
|
||||
),
|
||||
],
|
||||
appearance = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.wallpaper,
|
||||
title: l10n.appearanceDynamicColor,
|
||||
subtitle: l10n.appearanceDynamicColorSubtitle,
|
||||
keywords: const ['material you', 'system color'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.color_lens_outlined,
|
||||
title: l10n.sectionColor,
|
||||
keywords: const ['palette', 'seed color', 'accent'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.contrast,
|
||||
title: l10n.sectionTheme,
|
||||
subtitle:
|
||||
'${l10n.appearanceThemeSystem}, ${l10n.appearanceThemeLight}, '
|
||||
'${l10n.appearanceThemeDark}',
|
||||
keywords: const ['light mode', 'dark mode'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.brightness_2,
|
||||
title: l10n.appearanceAmoledDark,
|
||||
subtitle: l10n.appearanceAmoledDarkSubtitle,
|
||||
keywords: const ['black theme', 'oled'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.animation,
|
||||
title: l10n.appearanceHeroAnimations,
|
||||
subtitle: l10n.appearanceHeroAnimationsSubtitle,
|
||||
keywords: const ['transition', 'motion'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.blur_on,
|
||||
title: l10n.appearanceForceBlur,
|
||||
subtitle: l10n.appearanceForceBlurSubtitle,
|
||||
keywords: const ['backdrop blur', 'performance'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.language,
|
||||
title: l10n.appearanceLanguage,
|
||||
keywords: const ['locale', 'translation'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.grid_view_outlined,
|
||||
title: l10n.appearanceHistoryView,
|
||||
subtitle:
|
||||
'${l10n.appearanceHistoryViewList}, '
|
||||
'${l10n.appearanceHistoryViewGrid}',
|
||||
keywords: const ['layout', 'list', 'grid'],
|
||||
),
|
||||
],
|
||||
library = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.grid_view_rounded,
|
||||
title: l10n.libraryDefaultView,
|
||||
keywords: const ['library tab', 'default page'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq_rounded,
|
||||
title: l10n.trackAudioQuality,
|
||||
keywords: const ['quality label', 'bitrate', 'bit depth', 'kbps'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: l10n.libraryEnableLocalLibrary,
|
||||
subtitle: l10n.libraryEnableLocalLibrarySubtitle,
|
||||
keywords: const ['local music'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.folder_outlined,
|
||||
title: l10n.libraryFolder,
|
||||
keywords: const ['local path', 'scan folder'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.content_copy_outlined,
|
||||
title: l10n.libraryShowDuplicateIndicator,
|
||||
subtitle: l10n.libraryShowDuplicateIndicatorSubtitle,
|
||||
keywords: const ['duplicate badge'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.difference_outlined,
|
||||
title: l10n.libraryReviewDuplicates,
|
||||
subtitle: l10n.libraryReviewDuplicatesSubtitle,
|
||||
keywords: const ['duplicate songs'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.autorenew_rounded,
|
||||
title: l10n.libraryAutoScan,
|
||||
keywords: const ['automatic scan', 'daily', 'weekly'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.refresh,
|
||||
title: l10n.libraryScan,
|
||||
subtitle: l10n.libraryScanSubtitle,
|
||||
keywords: const ['rescan', 'refresh music'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.sync,
|
||||
title: l10n.libraryForceFullScan,
|
||||
subtitle: l10n.libraryForceFullScanSubtitle,
|
||||
keywords: const ['rescan all'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
title: l10n.libraryCleanupMissingFiles,
|
||||
subtitle: l10n.libraryCleanupMissingFilesSubtitle,
|
||||
keywords: const ['remove missing'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.delete_outline,
|
||||
title: l10n.libraryClear,
|
||||
subtitle: l10n.libraryClearSubtitle,
|
||||
keywords: const ['reset local library'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.open_in_new,
|
||||
title: l10n.libraryExternalPlayer,
|
||||
subtitle: l10n.libraryExternalPlayerSubtitle,
|
||||
keywords: const ['playback app'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: l10n.libraryBuiltInPreviewPlayer,
|
||||
subtitle: l10n.libraryBuiltInPreviewPlayerSubtitle,
|
||||
keywords: const ['internal player'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq,
|
||||
title: l10n.libraryPlaybackNormalization,
|
||||
subtitle: l10n.libraryPlaybackNormalizationSubtitle,
|
||||
keywords: const ['replaygain', 'volume'],
|
||||
),
|
||||
],
|
||||
metadata = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.sell_outlined,
|
||||
title: l10n.optionsEmbedMetadata,
|
||||
keywords: const ['tags', 'write metadata'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.people_alt_outlined,
|
||||
title: l10n.optionsArtistTagMode,
|
||||
keywords: const ['artist separator', 'multiple artists'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.image,
|
||||
title: l10n.optionsMaxQualityCover,
|
||||
subtitle: l10n.optionsMaxQualityCoverSubtitle,
|
||||
keywords: const ['artwork', 'album art', 'cover size'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq,
|
||||
title: l10n.optionsReplayGain,
|
||||
keywords: const ['volume normalization', 'loudness'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.source_outlined,
|
||||
title: l10n.metadataProvidersTitle,
|
||||
subtitle: l10n.metadataProvidersSubtitle,
|
||||
keywords: const ['metadata priority', 'tag provider'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.filter_list_outlined,
|
||||
title: l10n.downloadDeduplication,
|
||||
keywords: const ['duplicate download', 'same song'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: l10n.downloadQualityVariants,
|
||||
subtitle: l10n.downloadQualityVariantsDescription,
|
||||
keywords: const ['different quality', 'multiple versions'],
|
||||
),
|
||||
],
|
||||
lyrics = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.subtitles_outlined,
|
||||
title: l10n.optionsEmbedLyrics,
|
||||
keywords: const ['write lyrics', 'lrc'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.lyrics_outlined,
|
||||
title: l10n.lyricsMode,
|
||||
keywords: const ['synced', 'plain', 'lyrics type'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.source_outlined,
|
||||
title: l10n.lyricsProvidersTitle,
|
||||
keywords: const ['lyrics priority', 'lyrics source'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.translate_outlined,
|
||||
title: l10n.downloadNeteaseIncludeTranslation,
|
||||
keywords: const ['translated lyrics'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.text_fields_outlined,
|
||||
title: l10n.downloadNeteaseIncludeRomanization,
|
||||
keywords: const ['romanized lyrics', 'latin'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.record_voice_over_outlined,
|
||||
title: l10n.downloadAppleQqMultiPerson,
|
||||
keywords: const ['duet', 'multi singer'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq_outlined,
|
||||
title: l10n.downloadAppleElrcWordSync,
|
||||
keywords: const ['word synced', 'elrc'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.language_outlined,
|
||||
title: l10n.downloadMusixmatchLanguage,
|
||||
keywords: const ['lyrics locale'],
|
||||
),
|
||||
],
|
||||
download = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.extension_outlined,
|
||||
title: l10n.sectionService,
|
||||
keywords: const ['download provider', 'default service'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.tune,
|
||||
title: l10n.downloadAskBeforeDownload,
|
||||
subtitle: l10n.downloadAskQualitySubtitle,
|
||||
keywords: const ['quality picker', 'ask quality'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.audiotrack,
|
||||
title: l10n.downloadLossyFormat,
|
||||
keywords: const ['mp3', 'aac', 'opus'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.wifi,
|
||||
title: l10n.settingsDownloadNetwork,
|
||||
subtitle: l10n.settingsDownloadNetworkSubtitle,
|
||||
keywords: const ['wifi only', 'mobile data'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.dynamic_feed_outlined,
|
||||
title: l10n.settingsConcurrentDownloads,
|
||||
subtitle: l10n.settingsConcurrentDownloadsSubtitle,
|
||||
keywords: const ['parallel downloads', 'simultaneous'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.downloading_outlined,
|
||||
title: l10n.downloadNativeWorker,
|
||||
subtitle: l10n.downloadNativeWorkerSubtitle,
|
||||
keywords: const ['background worker', 'android worker'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.security_outlined,
|
||||
title: l10n.downloadNetworkCompatibilityMode,
|
||||
keywords: const ['network fallback', 'connection compatibility'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.lan_outlined,
|
||||
title: l10n.downloadAllowLocalNetwork,
|
||||
keywords: const ['lan', 'private network'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.manage_search,
|
||||
title: l10n.optionsPrimaryProvider,
|
||||
subtitle: l10n.optionsPrimaryProviderSubtitle,
|
||||
keywords: const ['search source', 'metadata source'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.tab_outlined,
|
||||
title: l10n.optionsDefaultSearchTab,
|
||||
subtitle: l10n.optionsDefaultSearchTabSubtitle,
|
||||
keywords: const ['track album artist playlist'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.sync,
|
||||
title: l10n.optionsAutoFallback,
|
||||
subtitle: l10n.optionsAutoFallbackSubtitle,
|
||||
keywords: const ['automatic provider fallback'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.extension_outlined,
|
||||
title: l10n.downloadFallbackExtensions,
|
||||
subtitle: l10n.downloadFallbackExtensionsSubtitle,
|
||||
keywords: const ['provider order'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.public,
|
||||
title: l10n.downloadSongLinkRegion,
|
||||
keywords: const ['country', 'region'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.file_download_outlined,
|
||||
title: l10n.settingsAutoExportFailed,
|
||||
subtitle: l10n.settingsAutoExportFailedSubtitle,
|
||||
keywords: const ['failed downloads', 'export errors'],
|
||||
),
|
||||
],
|
||||
files = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.folder_outlined,
|
||||
title: l10n.downloadDirectory,
|
||||
keywords: const ['download folder', 'saf', 'storage path'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.text_fields,
|
||||
title: l10n.downloadFilenameFormat,
|
||||
keywords: const ['file naming', 'template'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.music_note_outlined,
|
||||
title: l10n.downloadSingleFilenameFormat,
|
||||
keywords: const ['single naming', 'file template'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: l10n.downloadSeparateSinglesFolder,
|
||||
keywords: const ['singles directory'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.folder_outlined,
|
||||
title: l10n.downloadAlbumFolderStructure,
|
||||
keywords: const ['album directory', 'folder template'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.create_new_folder_outlined,
|
||||
title: l10n.downloadFolderOrganization,
|
||||
keywords: const ['organize folders', 'directory layout'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.playlist_play_outlined,
|
||||
title: l10n.downloadCreatePlaylistSourceFolder,
|
||||
keywords: const ['playlist directory'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.person_search_outlined,
|
||||
title: l10n.downloadUseAlbumArtistForFolders,
|
||||
keywords: const ['album artist directory'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.filter_alt_outlined,
|
||||
title: l10n.downloadArtistNameFilters,
|
||||
keywords: const ['artist folder filter'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.person_outline,
|
||||
title: l10n.downloadUsePrimaryArtistOnly,
|
||||
keywords: const ['main artist folder'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.group_remove_outlined,
|
||||
title: l10n.downloadFilterContributing,
|
||||
keywords: const ['featured artist folder'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.folder_special_outlined,
|
||||
title: l10n.allFilesAccess,
|
||||
subtitle: l10n.allFilesAccessDescription,
|
||||
keywords: const ['storage permission', 'manage files'],
|
||||
),
|
||||
],
|
||||
app = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.extension,
|
||||
title: l10n.optionsExtensionStore,
|
||||
subtitle: l10n.optionsExtensionStoreSubtitle,
|
||||
keywords: const ['repository', 'plugin store'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.open_in_browser,
|
||||
title: l10n.extensionVerificationBrowserTitle,
|
||||
keywords: const ['captcha', 'turnstile', 'external browser'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.system_update,
|
||||
title: l10n.optionsCheckUpdates,
|
||||
subtitle: l10n.optionsCheckUpdatesSubtitle,
|
||||
keywords: const ['app update'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.new_releases,
|
||||
title: l10n.optionsUpdateChannel,
|
||||
keywords: const ['stable', 'preview', 'beta'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
title: l10n.cleanupOrphanedDownloads,
|
||||
subtitle: l10n.cleanupOrphanedDownloadsSubtitle,
|
||||
keywords: const ['stale queue', 'missing download'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.delete_forever,
|
||||
title: l10n.optionsClearHistory,
|
||||
subtitle: l10n.optionsClearHistorySubtitle,
|
||||
keywords: const ['delete history'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.history_toggle_off_outlined,
|
||||
title: l10n.settingsSaveDownloadHistory,
|
||||
subtitle: l10n.settingsSaveDownloadHistorySubtitle,
|
||||
keywords: const ['remember downloads'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.bug_report,
|
||||
title: l10n.optionsDetailedLogging,
|
||||
keywords: const ['debug logs', 'diagnostics'],
|
||||
),
|
||||
],
|
||||
cache = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.folder_outlined,
|
||||
title: l10n.cacheAppDirectory,
|
||||
subtitle: l10n.cacheAppDirectoryDesc,
|
||||
keywords: const ['application cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.timer_outlined,
|
||||
title: l10n.cacheTempDirectory,
|
||||
subtitle: l10n.cacheTempDirectoryDesc,
|
||||
keywords: const ['temporary files'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.image_outlined,
|
||||
title: l10n.cacheCoverImage,
|
||||
subtitle: l10n.cacheCoverImageDesc,
|
||||
keywords: const ['artwork cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: l10n.cacheLibraryCover,
|
||||
subtitle: l10n.cacheLibraryCoverDesc,
|
||||
keywords: const ['local artwork cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq_outlined,
|
||||
title: l10n.cacheAudioAnalysis,
|
||||
subtitle: l10n.cacheAudioAnalysisDesc,
|
||||
keywords: const ['spectrogram cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.explore_outlined,
|
||||
title: l10n.cacheExploreFeed,
|
||||
subtitle: l10n.cacheExploreFeedDesc,
|
||||
keywords: const ['home feed cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.memory_outlined,
|
||||
title: l10n.cacheTrackLookup,
|
||||
subtitle: l10n.cacheTrackLookupDesc,
|
||||
keywords: const ['search cache'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
title: l10n.cacheCleanupUnused,
|
||||
subtitle: l10n.cacheCleanupUnusedDesc,
|
||||
keywords: const ['clear cache', 'free space'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.delete_sweep_outlined,
|
||||
title: l10n.cacheClearAll,
|
||||
keywords: const ['delete all cache'],
|
||||
),
|
||||
],
|
||||
backup = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.vpn_key_outlined,
|
||||
title: l10n.backupIncludeSecrets,
|
||||
subtitle: l10n.backupIncludeSecretsDescription,
|
||||
keywords: const ['tokens', 'credentials'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.ios_share,
|
||||
title: l10n.backupExportButton,
|
||||
subtitle: l10n.backupExportSectionDescription,
|
||||
keywords: const ['create backup', 'save settings'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.settings_backup_restore,
|
||||
title: l10n.backupImportButton,
|
||||
subtitle: l10n.backupImportSectionDescription,
|
||||
keywords: const ['restore backup', 'load settings'],
|
||||
),
|
||||
],
|
||||
logs = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.filter_list,
|
||||
title: l10n.logFilterLevel,
|
||||
subtitle: l10n.logFilterBySeverity,
|
||||
keywords: const ['severity', 'error warning debug'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.search,
|
||||
title: l10n.logSearchHint,
|
||||
keywords: const ['find logs'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.copy,
|
||||
title: l10n.logCopyLogs,
|
||||
keywords: const ['clipboard'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.share,
|
||||
title: l10n.logShareLogs,
|
||||
keywords: const ['export logs'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.delete_outline,
|
||||
title: l10n.logClearLogs,
|
||||
keywords: const ['delete logs'],
|
||||
),
|
||||
],
|
||||
about = [
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.info_outline,
|
||||
title: l10n.aboutVersion,
|
||||
keywords: const ['build number', 'app version'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.people_outline,
|
||||
title: l10n.aboutContributors,
|
||||
keywords: const ['developers', 'credits'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.translate,
|
||||
title: l10n.aboutTranslators,
|
||||
keywords: const ['translation credits'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.code,
|
||||
title: l10n.aboutMobileSource,
|
||||
keywords: const ['github', 'source code'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.bug_report_outlined,
|
||||
title: l10n.aboutReportIssue,
|
||||
subtitle: l10n.aboutReportIssueSubtitle,
|
||||
keywords: const ['bug report'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.lightbulb_outline,
|
||||
title: l10n.aboutFeatureRequest,
|
||||
subtitle: l10n.aboutFeatureRequestSubtitle,
|
||||
keywords: const ['suggestion', 'idea'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.telegram,
|
||||
title: l10n.aboutTelegramChannel,
|
||||
subtitle: l10n.aboutTelegramChannelSubtitle,
|
||||
keywords: const ['social'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.forum_outlined,
|
||||
title: l10n.aboutTelegramChat,
|
||||
subtitle: l10n.aboutTelegramChatSubtitle,
|
||||
keywords: const ['community', 'support chat'],
|
||||
),
|
||||
];
|
||||
|
||||
final List<SettingsSearchEntry> extensions;
|
||||
final List<SettingsSearchEntry> appearance;
|
||||
final List<SettingsSearchEntry> library;
|
||||
final List<SettingsSearchEntry> metadata;
|
||||
final List<SettingsSearchEntry> lyrics;
|
||||
final List<SettingsSearchEntry> download;
|
||||
final List<SettingsSearchEntry> files;
|
||||
final List<SettingsSearchEntry> app;
|
||||
final List<SettingsSearchEntry> cache;
|
||||
final List<SettingsSearchEntry> backup;
|
||||
final List<SettingsSearchEntry> logs;
|
||||
final List<SettingsSearchEntry> about;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import 'package:spotiflac_android/screens/settings/library_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/log_screen.dart';
|
||||
import 'package:spotiflac_android/screens/settings/lyrics_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/metadata_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
||||
@@ -31,6 +32,7 @@ class _Destination {
|
||||
required this.subtitle,
|
||||
required this.pageBuilder,
|
||||
this.keywords = const [],
|
||||
this.searchEntries = const [],
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
@@ -41,14 +43,20 @@ class _Destination {
|
||||
/// Extra search terms for things the title does not spell out (e.g. "SAF"
|
||||
/// for the Files page), so a user can find a page by what it does.
|
||||
final List<String> keywords;
|
||||
final List<SettingsSearchEntry> searchEntries;
|
||||
|
||||
bool matches(String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final haystack = [title, subtitle, ...keywords].join(' ').toLowerCase();
|
||||
return haystack.contains(query);
|
||||
bool matches(SettingsSearchQuery query) {
|
||||
return query.matches([title, subtitle, ...keywords]);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchResult {
|
||||
const _SearchResult(this.destination, [this.entry]);
|
||||
|
||||
final _Destination destination;
|
||||
final SettingsSearchEntry? entry;
|
||||
}
|
||||
|
||||
/// Destinations that stay visually connected inside one settings card.
|
||||
class _Group {
|
||||
const _Group({required this.destinations});
|
||||
@@ -65,11 +73,15 @@ class SettingsTab extends ConsumerStatefulWidget {
|
||||
|
||||
class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
AppLocalizations? _cachedLocalizations;
|
||||
List<_Group>? _cachedGroups;
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -77,7 +89,12 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
/// without adding section labels that compete with the page title.
|
||||
List<_Group> _groups(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return [
|
||||
if (identical(_cachedLocalizations, l10n) && _cachedGroups != null) {
|
||||
return _cachedGroups!;
|
||||
}
|
||||
|
||||
final searchCatalog = SettingsSearchCatalog(l10n);
|
||||
final groups = [
|
||||
_Group(
|
||||
destinations: [
|
||||
_Destination(
|
||||
@@ -96,6 +113,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsExtensions,
|
||||
subtitle: l10n.settingsExtensionsSubtitle,
|
||||
keywords: const ['plugin', 'provider', 'priority', 'store'],
|
||||
searchEntries: searchCatalog.extensions,
|
||||
pageBuilder: () => const ExtensionsPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -113,6 +131,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
'layout',
|
||||
'grid',
|
||||
],
|
||||
searchEntries: searchCatalog.appearance,
|
||||
pageBuilder: () => const AppearanceSettingsPage(),
|
||||
),
|
||||
],
|
||||
@@ -130,6 +149,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
'playback',
|
||||
'duplicate',
|
||||
],
|
||||
searchEntries: searchCatalog.library,
|
||||
pageBuilder: () => const LibrarySettingsPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -137,6 +157,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsMetadata,
|
||||
subtitle: l10n.settingsMetadataSubtitle,
|
||||
keywords: const ['tag', 'cover', 'artwork', 'isrc', 'provider'],
|
||||
searchEntries: searchCatalog.metadata,
|
||||
pageBuilder: () => const MetadataSettingsPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -144,6 +165,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsLyrics,
|
||||
subtitle: l10n.settingsLyricsSubtitle,
|
||||
keywords: const ['lrc', 'synced', 'provider'],
|
||||
searchEntries: searchCatalog.lyrics,
|
||||
pageBuilder: () => const LyricsSettingsPage(),
|
||||
),
|
||||
],
|
||||
@@ -163,6 +185,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
'service',
|
||||
'region',
|
||||
],
|
||||
searchEntries: searchCatalog.download,
|
||||
pageBuilder: () => const DownloadSettingsPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -177,6 +200,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
'path',
|
||||
'permission',
|
||||
],
|
||||
searchEntries: searchCatalog.files,
|
||||
pageBuilder: () => const FilesSettingsPage(),
|
||||
),
|
||||
],
|
||||
@@ -188,6 +212,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsApp,
|
||||
subtitle: l10n.settingsAppSubtitle,
|
||||
keywords: const ['update', 'channel', 'debug', 'logging'],
|
||||
searchEntries: searchCatalog.app,
|
||||
pageBuilder: () => const AppSettingsPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -195,6 +220,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsCache,
|
||||
subtitle: l10n.settingsCacheSubtitle,
|
||||
keywords: const ['clear', 'space', 'image', 'temp'],
|
||||
searchEntries: searchCatalog.cache,
|
||||
pageBuilder: () => const CacheManagementPage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -202,6 +228,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsBackup,
|
||||
subtitle: l10n.settingsBackupSubtitle,
|
||||
keywords: const ['export', 'import', 'restore', 'json'],
|
||||
searchEntries: searchCatalog.backup,
|
||||
pageBuilder: () => const BackupRestorePage(),
|
||||
),
|
||||
_Destination(
|
||||
@@ -209,6 +236,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.logTitle,
|
||||
subtitle: l10n.settingsLogsSubtitle,
|
||||
keywords: const ['debug', 'error', 'report'],
|
||||
searchEntries: searchCatalog.logs,
|
||||
pageBuilder: () => const LogScreen(),
|
||||
),
|
||||
],
|
||||
@@ -220,16 +248,33 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
title: l10n.settingsAbout,
|
||||
subtitle: '${l10n.aboutVersion} ${AppInfo.displayVersion}',
|
||||
keywords: const ['version', 'license', 'contributor'],
|
||||
searchEntries: searchCatalog.about,
|
||||
pageBuilder: () => const AboutPage(),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
_cachedLocalizations = l10n;
|
||||
_cachedGroups = groups;
|
||||
return groups;
|
||||
}
|
||||
|
||||
void _navigateTo(BuildContext context, Widget page) {
|
||||
Future<void> _navigateTo(BuildContext context, Widget page) async {
|
||||
_searchFocusNode.unfocus();
|
||||
_searchFocusNode.canRequestFocus = false;
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
Navigator.of(context).push(slidePageRoute<void>(page: page));
|
||||
await Navigator.of(context).push(slidePageRoute<void>(page: page));
|
||||
if (!mounted) return;
|
||||
|
||||
// A route's focus scope remembers its previously focused child. Keep the
|
||||
// search field out of that restoration cycle while the child page is open,
|
||||
// then re-enable it without requesting focus when Settings becomes active.
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_searchFocusNode.canRequestFocus = true;
|
||||
_searchFocusNode.unfocus();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _itemFor(_Destination destination, {required bool showDivider}) {
|
||||
@@ -242,12 +287,30 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _searchItemFor(_SearchResult result, {required bool showDivider}) {
|
||||
final entry = result.entry;
|
||||
if (entry == null) {
|
||||
return _itemFor(result.destination, showDivider: showDivider);
|
||||
}
|
||||
|
||||
return SettingsItem(
|
||||
key: ValueKey(
|
||||
'settings-search:${result.destination.title}:${entry.title}',
|
||||
),
|
||||
icon: entry.icon,
|
||||
title: entry.title,
|
||||
subtitle: result.destination.title,
|
||||
showDivider: showDivider,
|
||||
onTap: () => _navigateTo(context, result.destination.pageBuilder()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.tokens;
|
||||
final bottomInset = context.navBarBottomInset;
|
||||
final wideInset = wideListInset(context);
|
||||
final query = _query.trim().toLowerCase();
|
||||
final query = SettingsSearchQuery(_query);
|
||||
final groups = _groups(context);
|
||||
|
||||
final margin = EdgeInsets.fromLTRB(
|
||||
@@ -275,9 +338,13 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
),
|
||||
];
|
||||
} else {
|
||||
final matches = [
|
||||
final matches = <_SearchResult>[
|
||||
for (final group in groups)
|
||||
...group.destinations.where((d) => d.matches(query)),
|
||||
for (final destination in group.destinations) ...[
|
||||
if (destination.matches(query)) _SearchResult(destination),
|
||||
for (final entry in destination.searchEntries)
|
||||
if (entry.matches(query)) _SearchResult(destination, entry),
|
||||
],
|
||||
];
|
||||
body = matches.isEmpty
|
||||
? [
|
||||
@@ -300,7 +367,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
margin: margin,
|
||||
children: [
|
||||
for (var i = 0; i < matches.length; i++)
|
||||
_itemFor(
|
||||
_searchItemFor(
|
||||
matches[i],
|
||||
showDivider: i != matches.length - 1,
|
||||
),
|
||||
@@ -323,6 +390,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
),
|
||||
child: AppSearchField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
hintText: context.l10n.settingsSearchHint,
|
||||
clearTooltip: context.l10n.dialogClear,
|
||||
onChanged: (value) => setState(() => _query = value),
|
||||
|
||||
Reference in New Issue
Block a user