From cb8064a317d56d8ccd97e79dadc70f075f8e56f0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 26 Jul 2026 18:52:26 +0700 Subject: [PATCH] refactor(dart): consolidate duplicated lookup, filename, and search-provider helpers --- lib/providers/download_queue_provider.dart | 23 +++-------- ...download_queue_provider_native_worker.dart | 23 +++-------- .../download_queue_provider_paths.dart | 28 +++++++++++++ lib/providers/extension_provider.dart | 29 +++++++------- lib/providers/track_provider.dart | 30 +------------- lib/screens/home_search_logic.dart | 30 +------------- .../settings/download_settings_page.dart | 15 +------ lib/services/history_database.dart | 34 +++++----------- lib/services/library_database.dart | 30 +++++--------- lib/services/sqlite_helpers.dart | 31 +++++++++++++++ lib/widgets/audio_analysis_widget.dart | 39 +++++++------------ 11 files changed, 126 insertions(+), 186 deletions(-) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 61ab860f..1a555c3b 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2493,23 +2493,12 @@ class DownloadQueueNotifier extends Notifier { baseFilenameFormat, ); if (isSafMode) { - final baseName = await PlatformBridge.buildFilename( - effectiveFilenameFormat, - _filenameMetadataForTrack( - trackToDownload, - quality: quality, - qualityVariant: item.preserveQualityVariant - ? qualityVariantStagingLabel(item.id) - : '', - playlistPosition: _validPlaylistPosition(item), - ), - ); - safFileName = await _buildSafFileName( - baseName, - safOutputExt, - qualityVariant: item.preserveQualityVariant - ? qualityVariantStagingLabel(item.id) - : '', + safFileName = await _buildSafFileNameForItem( + item, + trackToDownload, + filenameFormat: effectiveFilenameFormat, + quality: quality, + outputExt: safOutputExt, ); safBaseName = safFileName.replaceFirst(RegExp(r'\.[^.]+$'), ''); } diff --git a/lib/providers/download_queue_provider_native_worker.dart b/lib/providers/download_queue_provider_native_worker.dart index 52bf0b62..789502c9 100644 --- a/lib/providers/download_queue_provider_native_worker.dart +++ b/lib/providers/download_queue_provider_native_worker.dart @@ -680,23 +680,12 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier { baseFilenameFormat, ); if (isSafMode) { - final baseName = await PlatformBridge.buildFilename( - effectiveFilenameFormat, - _filenameMetadataForTrack( - item.track, - quality: quality, - qualityVariant: item.preserveQualityVariant - ? qualityVariantStagingLabel(item.id) - : '', - playlistPosition: _validPlaylistPosition(item), - ), - ); - safFileName = await _buildSafFileName( - baseName, - safOutputExt, - qualityVariant: item.preserveQualityVariant - ? qualityVariantStagingLabel(item.id) - : '', + safFileName = await _buildSafFileNameForItem( + item, + item.track, + filenameFormat: effectiveFilenameFormat, + quality: quality, + outputExt: safOutputExt, ); } diff --git a/lib/providers/download_queue_provider_paths.dart b/lib/providers/download_queue_provider_paths.dart index 981ed193..8071306c 100644 --- a/lib/providers/download_queue_provider_paths.dart +++ b/lib/providers/download_queue_provider_paths.dart @@ -207,6 +207,34 @@ extension _DownloadQueuePaths on DownloadQueueNotifier { return parts.join('/'); } + /// Renders the SAF file name for [item]: filename template → sanitized, + /// byte-limited SAF name (quality-variant aware). + Future _buildSafFileNameForItem( + DownloadItem item, + Track track, { + required String filenameFormat, + required String quality, + required String outputExt, + }) async { + final qualityVariant = item.preserveQualityVariant + ? qualityVariantStagingLabel(item.id) + : ''; + final baseName = await PlatformBridge.buildFilename( + filenameFormat, + _filenameMetadataForTrack( + track, + quality: quality, + qualityVariant: qualityVariant, + playlistPosition: _validPlaylistPosition(item), + ), + ); + return _buildSafFileName( + baseName, + outputExt, + qualityVariant: qualityVariant, + ); + } + Future _buildSafFileName( String baseName, String outputExt, { diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 98d18236..c5b2ad6b 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -63,6 +63,21 @@ List? _tryDecodeStringListPreference(String rawJson, String key) { } } +/// First enabled custom-search extension, preferring ones marked primary. +Extension? defaultSearchExtension(List extensions) { + return extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .firstOrNull ?? + extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .firstOrNull; +} + class Extension { final String id; final String name; @@ -1247,19 +1262,7 @@ class ExtensionNotifier extends Notifier { } String? _firstEnabledSearchProviderId() { - return state.extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .map((ext) => ext.id) - .firstOrNull ?? - state.extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .map((ext) => ext.id) - .firstOrNull; + return defaultSearchExtension(state.extensions)?.id; } String? _replacedBuiltInProviderFor( diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index d0542dcd..530020df 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -316,20 +316,7 @@ class TrackNotifier extends Notifier { if (explicitProvider != null && explicitProvider.isNotEmpty) { resolvedProvider = explicitProvider; } else { - resolvedProvider = - extensionState.extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .map((ext) => ext.id) - .firstOrNull ?? - extensionState.extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .map((ext) => ext.id) - .firstOrNull; + resolvedProvider = defaultSearchExtension(extensionState.extensions)?.id; } if (resolvedProvider != null && @@ -339,20 +326,7 @@ class TrackNotifier extends Notifier { ) && settings.searchProvider?.trim() == resolvedProvider) { ref.read(settingsProvider.notifier).setSearchProvider(null); - resolvedProvider = - extensionState.extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .map((ext) => ext.id) - .firstOrNull ?? - extensionState.extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .map((ext) => ext.id) - .firstOrNull; + resolvedProvider = defaultSearchExtension(extensionState.extensions)?.id; } final isEnabledExtensionProvider = diff --git a/lib/screens/home_search_logic.dart b/lib/screens/home_search_logic.dart index 91602bdb..32042f8c 100644 --- a/lib/screens/home_search_logic.dart +++ b/lib/screens/home_search_logic.dart @@ -109,19 +109,7 @@ class HomeSearchProviderPolicy { const HomeSearchProviderPolicy._(); static Extension? defaultExtension(List extensions) { - return extensions - .where( - (extension) => - extension.enabled && - extension.hasCustomSearch && - extension.searchBehavior?.primary == true, - ) - .firstOrNull ?? - extensions - .where( - (extension) => extension.enabled && extension.hasCustomSearch, - ) - .firstOrNull; + return defaultSearchExtension(extensions); } static String? resolveProvider( @@ -146,21 +134,7 @@ class HomeSearchProviderPolicy { String? explicitSearchProvider, List extensions, ) { - final explicit = explicitSearchProvider?.trim(); - if (explicit != null && - explicit.isNotEmpty && - extensions.any( - (extension) => - extension.enabled && - extension.hasCustomSearch && - extension.id == explicit, - )) { - return true; - } - - return extensions.any( - (extension) => extension.enabled && extension.hasCustomSearch, - ); + return resolveProvider(explicitSearchProvider, extensions) != null; } static String? sanitizeFilter( diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index a982bebc..0fea8247 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1061,19 +1061,6 @@ class _ServiceChip extends StatelessWidget { class _MetadataSourceSelector extends ConsumerWidget { const _MetadataSourceSelector(); - Extension? _defaultSearchExtension(List extensions) { - return extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .firstOrNull ?? - extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .firstOrNull; - } @override Widget build(BuildContext context, WidgetRef ref) { @@ -1082,7 +1069,7 @@ class _MetadataSourceSelector extends ConsumerWidget { final extState = ref.watch(extensionProvider); final rawSearchProvider = settings.searchProvider?.trim() ?? ''; - final primarySearchExtension = _defaultSearchExtension(extState.extensions); + final primarySearchExtension = defaultSearchExtension(extState.extensions); final defaultProviderTarget = primarySearchExtension?.displayName ?? context.l10n.extensionsNoCustomSearch; diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index d821d5cc..abfd8b89 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -760,30 +760,16 @@ class HistoryDatabase { String column, Iterable rawValues, Map> destination, - ) async { - final values = rawValues - .where((value) => value.isNotEmpty) - .toSet() - .toList(); - const chunkSize = 450; - for (var start = 0; start < values.length; start += chunkSize) { - final end = start + chunkSize < values.length - ? start + chunkSize - : values.length; - final chunk = values.sublist(start, end); - final placeholders = List.filled(chunk.length, '?').join(','); - final rows = await db.rawQuery( - 'SELECT * FROM history WHERE $column IN ($placeholders) ' - 'ORDER BY downloaded_at DESC', - chunk, - ); - for (final row in rows) { - final key = row[column] as String?; - if (key != null && key.isNotEmpty) { - destination.putIfAbsent(key, () => _dbRowToJson(row)); - } - } - } + ) { + return sqlite.loadRowsByColumn( + db, + table: 'history', + column: column, + rawValues: rawValues, + destination: destination, + mapRow: _dbRowToJson, + orderBy: 'downloaded_at DESC', + ); } final spotifyCandidates = requests.expand( diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index b03fd6d2..2fc7c38b 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -1437,27 +1437,15 @@ class LibraryDatabase { String column, Iterable rawValues, Map> destination, - ) async { - final values = rawValues - .where((value) => value.isNotEmpty) - .toSet() - .toList(); - const chunkSize = 450; - for (var start = 0; start < values.length; start += chunkSize) { - final end = (start + chunkSize).clamp(0, values.length); - final chunk = values.sublist(start, end); - final placeholders = List.filled(chunk.length, '?').join(','); - final rows = await db.rawQuery( - 'SELECT * FROM library WHERE $column IN ($placeholders)', - chunk, - ); - for (final row in rows) { - final key = row[column] as String?; - if (key != null && key.isNotEmpty) { - destination.putIfAbsent(key, () => _dbRowToJson(row)); - } - } - } + ) { + return sqlite.loadRowsByColumn( + db, + table: 'library', + column: column, + rawValues: rawValues, + destination: destination, + mapRow: _dbRowToJson, + ); } await Future.wait([ diff --git a/lib/services/sqlite_helpers.dart b/lib/services/sqlite_helpers.dart index 03213d44..2c5be566 100644 --- a/lib/services/sqlite_helpers.dart +++ b/lib/services/sqlite_helpers.dart @@ -59,6 +59,37 @@ Future addColumnIfMissing( } } +/// Loads rows whose [column] matches any of [rawValues] (chunked IN clauses) +/// into [destination], keeping the first row seen per value. +Future loadRowsByColumn( + DatabaseExecutor db, { + required String table, + required String column, + required Iterable rawValues, + required Map> destination, + required Map Function(Map row) mapRow, + String? orderBy, +}) async { + final values = rawValues.where((value) => value.isNotEmpty).toSet().toList(); + const chunkSize = 450; + for (var start = 0; start < values.length; start += chunkSize) { + final end = (start + chunkSize).clamp(0, values.length); + final chunk = values.sublist(start, end); + final placeholders = List.filled(chunk.length, '?').join(','); + final rows = await db.rawQuery( + 'SELECT * FROM $table WHERE $column IN ($placeholders)' + '${orderBy == null ? '' : ' ORDER BY $orderBy'}', + chunk, + ); + for (final row in rows) { + final key = row[column] as String?; + if (key != null && key.isNotEmpty) { + destination.putIfAbsent(key, () => mapRow(row)); + } + } + } +} + Future createPathKeyTable(DatabaseExecutor db, String table) async { await db.execute(''' CREATE TABLE IF NOT EXISTS $table ( diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index 8d56b8d1..3db06b48 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -398,18 +398,7 @@ class _AudioAnalysisCardState extends State { widget.filePath, channel: _spectrogramChannel, ); - if (image == null) { - final artifact = await _generateSpectrogramForFile( - widget.filePath, - channel: _spectrogramChannel, - ); - image = artifact.image; - await _saveSpectrogramToCache( - widget.filePath, - image, - channel: _spectrogramChannel, - ); - } + image ??= await _generateAndCacheSpectrogram(); if (mounted) { setState(() { _spectrogramImage?.dispose(); @@ -426,6 +415,19 @@ class _AudioAnalysisCardState extends State { } } + Future _generateAndCacheSpectrogram() async { + final artifact = await _generateSpectrogramForFile( + widget.filePath, + channel: _spectrogramChannel, + ); + await _saveSpectrogramToCache( + widget.filePath, + artifact.image, + channel: _spectrogramChannel, + ); + return artifact.image; + } + Future _analyze({bool forceRefresh = false}) async { if (_analyzing) return; setState(() { @@ -470,18 +472,7 @@ class _AudioAnalysisCardState extends State { ); } - if (image == null) { - final artifact = await _generateSpectrogramForFile( - widget.filePath, - channel: _spectrogramChannel, - ); - image = artifact.image; - await _saveSpectrogramToCache( - widget.filePath, - image, - channel: _spectrogramChannel, - ); - } + image ??= await _generateAndCacheSpectrogram(); if (mounted) { setState(() {