mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
refactor(dart): consolidate duplicated lookup, filename, and search-provider helpers
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(() {
|
||||
|
||||
Reference in New Issue
Block a user