From 6a7eef69568772ba0377344d64103c9b80c7f8f0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 11 Feb 2026 12:28:50 +0700 Subject: [PATCH 1/3] chore: add Elias el Autentico to supporters list --- 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) { From fdbb474763528f73d1e8fc288377c2b69cc39091 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:30:57 +0000 Subject: [PATCH 2/3] chore(deps): update dependency go to 1.26 --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64295ef7..3198bbcd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: "1.26" cache-dependency-path: go_backend/go.sum # Cache Gradle for faster builds @@ -174,7 +174,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: "1.26" cache-dependency-path: go_backend/go.sum # Cache CocoaPods From eefbb63299eaf10e09f4ff6f61f91029cf2ec6e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:31:18 +0000 Subject: [PATCH 3/3] fix(deps): update go dependencies --- go_backend/go.mod | 16 ++++++++-------- go_backend/go.sum | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/go_backend/go.mod b/go_backend/go.mod index a7f0daef..d903afd6 100644 --- a/go_backend/go.mod +++ b/go_backend/go.mod @@ -2,7 +2,7 @@ module github.com/zarz/spotiflac_android/go_backend go 1.25.0 -toolchain go1.25.7 +toolchain go1.26.0 require ( github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 @@ -10,8 +10,8 @@ require ( github.com/go-flac/flacvorbis/v2 v2.0.2 github.com/go-flac/go-flac/v2 v2.0.4 github.com/refraction-networking/utls v1.8.2 - golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 - golang.org/x/net v0.49.0 + golang.org/x/mobile v0.0.0-20260209203831-923679eb55af + golang.org/x/net v0.50.0 ) require ( @@ -20,10 +20,10 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/klauspost/compress v1.17.4 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect ) diff --git a/go_backend/go.sum b/go_backend/go.sum index 3840d5bf..e1f185bb 100644 --- a/go_backend/go.sum +++ b/go_backend/go.sum @@ -30,20 +30,34 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 h1:NiJtT7g4ncNFVjVZMAYNBrPSNhIjFYPj8UKA8MEw2A4= golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3/go.mod h1:wReH3Q1agKmmLapipWFnd4NSs8KPz3fK6mSEZjXLkrg= +golang.org/x/mobile v0.0.0-20260209203831-923679eb55af h1:VqXrZNyqFISxo0rNDFZQlRDRIp7RXSJDeh/LbrK+W1k= +golang.org/x/mobile v0.0.0-20260209203831-923679eb55af/go.mod h1:tbwefIr7RlQD1OpZ0KEZ9nux/uiihAOGdafgZfJkmII= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=