From abc599d7f98b9df46da4d7c9a89922ba0fe7b654 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 12:28:50 +0700 Subject: [PATCH] refactor: migrate queue_tab cover resolver to shared service, add supporter --- lib/screens/queue_tab.dart | 190 +++--------------- lib/screens/settings/donate_page.dart | 3 +- .../downloaded_embedded_cover_resolver.dart | 87 +++----- 3 files changed, 54 insertions(+), 226 deletions(-) diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 2eba8858..6b03573f 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -14,7 +14,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; -import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; import 'package:spotiflac_android/screens/local_album_screen.dart'; @@ -279,7 +279,7 @@ class _QueueTabState extends ConsumerState { final Set _pendingChecks = {}; static const int _maxCacheSize = 500; static const int _maxSearchIndexCacheSize = 4000; - static const int _maxDownloadedEmbeddedCoverCacheSize = 180; + bool _embeddedCoverRefreshScheduled = false; bool _isSelectionMode = false; final Set _selectedIds = {}; @@ -311,10 +311,6 @@ class _QueueTabState extends ConsumerState { _HistoryStats? _historyStatsCache; final Map _searchIndexCache = {}; final Map _localSearchIndexCache = {}; - final Map _downloadedEmbeddedCoverCache = {}; - final Set _pendingDownloadedCoverExtract = {}; - final Set _pendingDownloadedCoverRefresh = {}; - final Set _failedDownloadedCoverExtract = {}; Map> _filteredHistoryCache = const {}; List? _filterItemsCache; String _filterQueryCache = ''; @@ -361,13 +357,6 @@ class _QueueTabState extends ConsumerState { @override void dispose() { - for (final coverPath in _downloadedEmbeddedCoverCache.values) { - _cleanupTempCoverPathSync(coverPath); - } - _downloadedEmbeddedCoverCache.clear(); - _pendingDownloadedCoverExtract.clear(); - _pendingDownloadedCoverRefresh.clear(); - _failedDownloadedCoverExtract.clear(); for (final notifier in _fileExistsNotifiers.values) { notifier.dispose(); } @@ -425,12 +414,7 @@ class _QueueTabState extends ConsumerState { .map((item) => _cleanFilePath(item.filePath)) .where((path) => path.isNotEmpty) .toSet(); - final staleKeys = _downloadedEmbeddedCoverCache.keys - .where((path) => !validPaths.contains(path)) - .toList(growable: false); - for (final key in staleKeys) { - _invalidateDownloadedEmbeddedCover(key); - } + DownloadedEmbeddedCoverResolver.invalidatePathsNotIn(validPaths); } _requestFilterRefresh(); } @@ -794,69 +778,22 @@ class _QueueTabState extends ConsumerState { /// Strip EXISTS: prefix from file path (legacy history items) String _cleanFilePath(String? filePath) { - if (filePath == null) return ''; - if (filePath.startsWith('EXISTS:')) { - return filePath.substring(7); - } - return filePath; - } - - void _cleanupTempCoverPathSync(String? coverPath) { - if (coverPath == null || coverPath.isEmpty) return; - try { - final file = File(coverPath); - if (file.existsSync()) { - file.deleteSync(); - } - final parent = file.parent; - if (parent.existsSync()) { - parent.deleteSync(recursive: true); - } - } catch (_) {} - } - - void _invalidateDownloadedEmbeddedCover(String? filePath) { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return; - - final cachedPath = _downloadedEmbeddedCoverCache.remove(cleanPath); - _pendingDownloadedCoverExtract.remove(cleanPath); - _pendingDownloadedCoverRefresh.remove(cleanPath); - _failedDownloadedCoverExtract.remove(cleanPath); - _cleanupTempCoverPathSync(cachedPath); - } - - void _trimDownloadedEmbeddedCoverCache() { - while (_downloadedEmbeddedCoverCache.length > - _maxDownloadedEmbeddedCoverCacheSize) { - final oldestKey = _downloadedEmbeddedCoverCache.keys.first; - final removedPath = _downloadedEmbeddedCoverCache.remove(oldestKey); - _pendingDownloadedCoverExtract.remove(oldestKey); - _pendingDownloadedCoverRefresh.remove(oldestKey); - _failedDownloadedCoverExtract.remove(oldestKey); - _cleanupTempCoverPathSync(removedPath); - } + return DownloadedEmbeddedCoverResolver.cleanFilePath(filePath); } Future _readFileModTimeMillis(String? filePath) async { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return null; + return DownloadedEmbeddedCoverResolver.readFileModTimeMillis(filePath); + } - if (cleanPath.startsWith('content://')) { - try { - final modTimes = await PlatformBridge.getSafFileModTimes([cleanPath]); - return modTimes[cleanPath]; - } catch (_) { - return null; + void _onEmbeddedCoverChanged() { + if (!mounted || _embeddedCoverRefreshScheduled) return; + _embeddedCoverRefreshScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _embeddedCoverRefreshScheduled = false; + if (mounted) { + setState(() {}); } - } - - try { - final stat = await File(cleanPath).stat(); - return stat.modified.millisecondsSinceEpoch; - } catch (_) { - return null; - } + }); } Future _scheduleDownloadedEmbeddedCoverRefreshForPath( @@ -864,98 +801,19 @@ class _QueueTabState extends ConsumerState { int? beforeModTime, bool force = false, }) async { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return; - - if (!force) { - if (beforeModTime == null) { - return; - } - final afterModTime = await _readFileModTimeMillis(cleanPath); - if (afterModTime != null && afterModTime == beforeModTime) { - return; - } - } - - _pendingDownloadedCoverRefresh.add(cleanPath); - _failedDownloadedCoverExtract.remove(cleanPath); - if (mounted) { - setState(() {}); - } + await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( + filePath, + beforeModTime: beforeModTime, + force: force, + onChanged: _onEmbeddedCoverChanged, + ); } String? _resolveDownloadedEmbeddedCoverPath(String? filePath) { - final cleanPath = _cleanFilePath(filePath); - if (cleanPath.isEmpty) return null; - - if (_pendingDownloadedCoverRefresh.remove(cleanPath)) { - _ensureDownloadedEmbeddedCover(cleanPath, forceRefresh: true); - } - - final cachedPath = _downloadedEmbeddedCoverCache[cleanPath]; - if (cachedPath != null) { - if (File(cachedPath).existsSync()) { - return cachedPath; - } - _downloadedEmbeddedCoverCache.remove(cleanPath); - _cleanupTempCoverPathSync(cachedPath); - } - - return null; - } - - void _ensureDownloadedEmbeddedCover( - String cleanPath, { - bool forceRefresh = false, - }) { - if (cleanPath.isEmpty) return; - if (_pendingDownloadedCoverExtract.contains(cleanPath)) return; - if (!forceRefresh && _downloadedEmbeddedCoverCache.containsKey(cleanPath)) { - return; - } - if (!forceRefresh && _failedDownloadedCoverExtract.contains(cleanPath)) { - return; - } - - _pendingDownloadedCoverExtract.add(cleanPath); - Future.microtask(() async { - String? outputPath; - try { - final tempDir = await Directory.systemTemp.createTemp('library_cover_'); - outputPath = '${tempDir.path}${Platform.pathSeparator}cover.jpg'; - final result = await PlatformBridge.extractCoverToFile( - cleanPath, - outputPath, - ); - - final hasCover = - result['error'] == null && await File(outputPath).exists(); - if (!hasCover) { - _failedDownloadedCoverExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); - return; - } - - if (!mounted) { - _cleanupTempCoverPathSync(outputPath); - return; - } - - final previous = _downloadedEmbeddedCoverCache[cleanPath]; - _downloadedEmbeddedCoverCache[cleanPath] = outputPath; - _failedDownloadedCoverExtract.remove(cleanPath); - _trimDownloadedEmbeddedCoverCache(); - if (previous != null && previous != outputPath) { - _cleanupTempCoverPathSync(previous); - } - setState(() {}); - } catch (_) { - _failedDownloadedCoverExtract.add(cleanPath); - _cleanupTempCoverPathSync(outputPath); - } finally { - _pendingDownloadedCoverExtract.remove(cleanPath); - } - }); + return DownloadedEmbeddedCoverResolver.resolve( + filePath, + onChanged: _onEmbeddedCoverChanged, + ); } ValueListenable _fileExistsListenable(String? filePath) { diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index 1ec3e784..56a7aefe 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -204,8 +204,9 @@ class _RecentDonorsCard extends StatelessWidget { _DonorTile(name: 'Julian', colorScheme: colorScheme), _DonorTile(name: 'matt_3050', colorScheme: colorScheme), _DonorTile(name: 'Daniel', colorScheme: colorScheme), + _DonorTile(name: '283Fabio', colorScheme: colorScheme), _DonorTile( - name: '283Fabio', + name: 'Elias el Autentico', colorScheme: colorScheme, showDivider: false, ), diff --git a/lib/services/downloaded_embedded_cover_resolver.dart b/lib/services/downloaded_embedded_cover_resolver.dart index 3f598c2f..9b613510 100644 --- a/lib/services/downloaded_embedded_cover_resolver.dart +++ b/lib/services/downloaded_embedded_cover_resolver.dart @@ -19,18 +19,13 @@ class _EmbeddedCoverCacheEntry { /// It keeps a bounded in-memory cache and only refreshes extraction /// when the source file changed. class DownloadedEmbeddedCoverResolver { - static const int _maxCacheEntries = 160; - static const int _minModCheckIntervalMs = 1200; - static const int _minPreviewExistsCheckIntervalMs = 2200; + static const int _maxCacheEntries = 180; static final LinkedHashMap _cache = LinkedHashMap(); static final Set _pendingExtract = {}; - static final Set _pendingModCheck = {}; + static final Set _pendingRefresh = {}; static final Set _failedExtract = {}; - static final Map _lastModCheckMillis = {}; - static final Map _lastPreviewExistsCheckMillis = - {}; static String cleanFilePath(String? filePath) { if (filePath == null) return ''; @@ -65,27 +60,20 @@ class DownloadedEmbeddedCoverResolver { final cleanPath = cleanFilePath(filePath); if (cleanPath.isEmpty) return null; + if (_pendingRefresh.remove(cleanPath)) { + _ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged); + } + final cached = _cache[cleanPath]; if (cached != null) { - final now = DateTime.now().millisecondsSinceEpoch; - final lastPreviewCheck = _lastPreviewExistsCheckMillis[cleanPath] ?? 0; - final shouldVerifyExists = - now - lastPreviewCheck >= _minPreviewExistsCheckIntervalMs; - - if (!shouldVerifyExists || File(cached.previewPath).existsSync()) { - if (shouldVerifyExists) { - _lastPreviewExistsCheckMillis[cleanPath] = now; - } + if (File(cached.previewPath).existsSync()) { _touch(cleanPath, cached); - _scheduleModCheck(cleanPath, onChanged: onChanged); return cached.previewPath; } _cache.remove(cleanPath); - _lastPreviewExistsCheckMillis.remove(cleanPath); _cleanupTempCoverPathSync(cached.previewPath); } - _ensureCover(cleanPath, onChanged: onChanged); return null; } @@ -106,8 +94,9 @@ class DownloadedEmbeddedCoverResolver { } } + _pendingRefresh.add(cleanPath); _failedExtract.remove(cleanPath); - _ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged); + onChanged?.call(); } static void invalidate(String? filePath) { @@ -116,15 +105,30 @@ class DownloadedEmbeddedCoverResolver { final cached = _cache.remove(cleanPath); _pendingExtract.remove(cleanPath); - _pendingModCheck.remove(cleanPath); + _pendingRefresh.remove(cleanPath); _failedExtract.remove(cleanPath); - _lastModCheckMillis.remove(cleanPath); - _lastPreviewExistsCheckMillis.remove(cleanPath); if (cached != null) { _cleanupTempCoverPathSync(cached.previewPath); } } + static void invalidatePathsNotIn(Set validCleanPaths) { + if (validCleanPaths.isEmpty) { + final keys = _cache.keys.toList(growable: false); + for (final key in keys) { + invalidate(key); + } + return; + } + + final staleKeys = _cache.keys + .where((path) => !validCleanPaths.contains(path)) + .toList(growable: false); + for (final key in staleKeys) { + invalidate(key); + } + } + static void _touch(String cleanPath, _EmbeddedCoverCacheEntry entry) { _cache ..remove(cleanPath) @@ -139,44 +143,11 @@ class DownloadedEmbeddedCoverResolver { _cleanupTempCoverPathSync(removed.previewPath); } _pendingExtract.remove(oldestKey); - _pendingModCheck.remove(oldestKey); + _pendingRefresh.remove(oldestKey); _failedExtract.remove(oldestKey); - _lastModCheckMillis.remove(oldestKey); - _lastPreviewExistsCheckMillis.remove(oldestKey); } } - static void _scheduleModCheck(String cleanPath, {VoidCallback? onChanged}) { - if (_pendingModCheck.contains(cleanPath)) return; - - final now = DateTime.now().millisecondsSinceEpoch; - final lastCheck = _lastModCheckMillis[cleanPath] ?? 0; - if (now - lastCheck < _minModCheckIntervalMs) return; - _lastModCheckMillis[cleanPath] = now; - - _pendingModCheck.add(cleanPath); - Future.microtask(() async { - try { - final cached = _cache[cleanPath]; - if (cached == null) return; - - final currentModTime = await readFileModTimeMillis(cleanPath); - if (currentModTime != null && - cached.sourceModTimeMillis != null && - currentModTime != cached.sourceModTimeMillis) { - _ensureCover( - cleanPath, - forceRefresh: true, - knownModTime: currentModTime, - onChanged: onChanged, - ); - } - } finally { - _pendingModCheck.remove(cleanPath); - } - }); - } - static void _ensureCover( String cleanPath, { bool forceRefresh = false, @@ -218,8 +189,6 @@ class DownloadedEmbeddedCoverResolver { ); _touch(cleanPath, next); _failedExtract.remove(cleanPath); - _lastPreviewExistsCheckMillis[cleanPath] = - DateTime.now().millisecondsSinceEpoch; _trimCacheIfNeeded(); if (previous != null && previous.previewPath != outputPath) {