refactor(dart): consolidate duplicated lookup, filename, and search-provider helpers

This commit is contained in:
zarzet
2026-07-26 18:52:26 +07:00
parent a21d5bcb33
commit cb8064a317
11 changed files with 126 additions and 186 deletions
+6 -17
View File
@@ -2493,23 +2493,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
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'\.[^.]+$'), '');
}
@@ -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,
);
}
@@ -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<String> _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<String> _buildSafFileName(
String baseName,
String outputExt, {
+16 -13
View File
@@ -63,6 +63,21 @@ List<String>? _tryDecodeStringListPreference(String rawJson, String key) {
}
}
/// First enabled custom-search extension, preferring ones marked primary.
Extension? defaultSearchExtension(List<Extension> 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<ExtensionState> {
}
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(
+2 -28
View File
@@ -316,20 +316,7 @@ class TrackNotifier extends Notifier<TrackState> {
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<TrackState> {
) &&
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 =
+2 -28
View File
@@ -109,19 +109,7 @@ class HomeSearchProviderPolicy {
const HomeSearchProviderPolicy._();
static Extension? defaultExtension(List<Extension> 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<Extension> 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(
@@ -1061,19 +1061,6 @@ class _ServiceChip extends StatelessWidget {
class _MetadataSourceSelector extends ConsumerWidget {
const _MetadataSourceSelector();
Extension? _defaultSearchExtension(List<Extension> 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;
+10 -24
View File
@@ -760,30 +760,16 @@ class HistoryDatabase {
String column,
Iterable<String> rawValues,
Map<String, Map<String, dynamic>> 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(
+9 -21
View File
@@ -1437,27 +1437,15 @@ class LibraryDatabase {
String column,
Iterable<String> rawValues,
Map<String, Map<String, dynamic>> 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([
+31
View File
@@ -59,6 +59,37 @@ Future<void> addColumnIfMissing(
}
}
/// Loads rows whose [column] matches any of [rawValues] (chunked IN clauses)
/// into [destination], keeping the first row seen per value.
Future<void> loadRowsByColumn(
DatabaseExecutor db, {
required String table,
required String column,
required Iterable<String> rawValues,
required Map<String, Map<String, dynamic>> destination,
required Map<String, dynamic> Function(Map<String, Object?> 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<void> createPathKeyTable(DatabaseExecutor db, String table) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $table (
+15 -24
View File
@@ -398,18 +398,7 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
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<AudioAnalysisCard> {
}
}
Future<ui.Image> _generateAndCacheSpectrogram() async {
final artifact = await _generateSpectrogramForFile(
widget.filePath,
channel: _spectrogramChannel,
);
await _saveSpectrogramToCache(
widget.filePath,
artifact.image,
channel: _spectrogramChannel,
);
return artifact.image;
}
Future<void> _analyze({bool forceRefresh = false}) async {
if (_analyzing) return;
setState(() {
@@ -470,18 +472,7 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
);
}
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(() {