@@ -14,7 +14,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
1414import 'package:spotiflac_android/providers/settings_provider.dart' ;
1515import 'package:spotiflac_android/providers/local_library_provider.dart' ;
1616import 'package:spotiflac_android/services/library_database.dart' ;
17- import 'package:spotiflac_android/services/platform_bridge .dart' ;
17+ import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver .dart' ;
1818import 'package:spotiflac_android/screens/track_metadata_screen.dart' ;
1919import 'package:spotiflac_android/screens/downloaded_album_screen.dart' ;
2020import 'package:spotiflac_android/screens/local_album_screen.dart' ;
@@ -279,7 +279,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
279279 final Set <String > _pendingChecks = {};
280280 static const int _maxCacheSize = 500 ;
281281 static const int _maxSearchIndexCacheSize = 4000 ;
282- static const int _maxDownloadedEmbeddedCoverCacheSize = 180 ;
282+ bool _embeddedCoverRefreshScheduled = false ;
283283
284284 bool _isSelectionMode = false ;
285285 final Set <String > _selectedIds = {};
@@ -311,10 +311,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
311311 _HistoryStats ? _historyStatsCache;
312312 final Map <String , String > _searchIndexCache = {};
313313 final Map <String , String > _localSearchIndexCache = {};
314- final Map <String , String > _downloadedEmbeddedCoverCache = {};
315- final Set <String > _pendingDownloadedCoverExtract = {};
316- final Set <String > _pendingDownloadedCoverRefresh = {};
317- final Set <String > _failedDownloadedCoverExtract = {};
318314 Map <String , List <DownloadHistoryItem >> _filteredHistoryCache = const {};
319315 List <DownloadHistoryItem >? _filterItemsCache;
320316 String _filterQueryCache = '' ;
@@ -361,13 +357,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
361357
362358 @override
363359 void dispose () {
364- for (final coverPath in _downloadedEmbeddedCoverCache.values) {
365- _cleanupTempCoverPathSync (coverPath);
366- }
367- _downloadedEmbeddedCoverCache.clear ();
368- _pendingDownloadedCoverExtract.clear ();
369- _pendingDownloadedCoverRefresh.clear ();
370- _failedDownloadedCoverExtract.clear ();
371360 for (final notifier in _fileExistsNotifiers.values) {
372361 notifier.dispose ();
373362 }
@@ -425,12 +414,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
425414 .map ((item) => _cleanFilePath (item.filePath))
426415 .where ((path) => path.isNotEmpty)
427416 .toSet ();
428- final staleKeys = _downloadedEmbeddedCoverCache.keys
429- .where ((path) => ! validPaths.contains (path))
430- .toList (growable: false );
431- for (final key in staleKeys) {
432- _invalidateDownloadedEmbeddedCover (key);
433- }
417+ DownloadedEmbeddedCoverResolver .invalidatePathsNotIn (validPaths);
434418 }
435419 _requestFilterRefresh ();
436420 }
@@ -794,168 +778,42 @@ class _QueueTabState extends ConsumerState<QueueTab> {
794778
795779 /// Strip EXISTS: prefix from file path (legacy history items)
796780 String _cleanFilePath (String ? filePath) {
797- if (filePath == null ) return '' ;
798- if (filePath.startsWith ('EXISTS:' )) {
799- return filePath.substring (7 );
800- }
801- return filePath;
802- }
803-
804- void _cleanupTempCoverPathSync (String ? coverPath) {
805- if (coverPath == null || coverPath.isEmpty) return ;
806- try {
807- final file = File (coverPath);
808- if (file.existsSync ()) {
809- file.deleteSync ();
810- }
811- final parent = file.parent;
812- if (parent.existsSync ()) {
813- parent.deleteSync (recursive: true );
814- }
815- } catch (_) {}
816- }
817-
818- void _invalidateDownloadedEmbeddedCover (String ? filePath) {
819- final cleanPath = _cleanFilePath (filePath);
820- if (cleanPath.isEmpty) return ;
821-
822- final cachedPath = _downloadedEmbeddedCoverCache.remove (cleanPath);
823- _pendingDownloadedCoverExtract.remove (cleanPath);
824- _pendingDownloadedCoverRefresh.remove (cleanPath);
825- _failedDownloadedCoverExtract.remove (cleanPath);
826- _cleanupTempCoverPathSync (cachedPath);
781+ return DownloadedEmbeddedCoverResolver .cleanFilePath (filePath);
827782 }
828783
829- void _trimDownloadedEmbeddedCoverCache () {
830- while (_downloadedEmbeddedCoverCache.length >
831- _maxDownloadedEmbeddedCoverCacheSize) {
832- final oldestKey = _downloadedEmbeddedCoverCache.keys.first;
833- final removedPath = _downloadedEmbeddedCoverCache.remove (oldestKey);
834- _pendingDownloadedCoverExtract.remove (oldestKey);
835- _pendingDownloadedCoverRefresh.remove (oldestKey);
836- _failedDownloadedCoverExtract.remove (oldestKey);
837- _cleanupTempCoverPathSync (removedPath);
838- }
784+ Future <int ?> _readFileModTimeMillis (String ? filePath) async {
785+ return DownloadedEmbeddedCoverResolver .readFileModTimeMillis (filePath);
839786 }
840787
841- Future <int ?> _readFileModTimeMillis (String ? filePath) async {
842- final cleanPath = _cleanFilePath (filePath);
843- if (cleanPath.isEmpty) return null ;
844-
845- if (cleanPath.startsWith ('content://' )) {
846- try {
847- final modTimes = await PlatformBridge .getSafFileModTimes ([cleanPath]);
848- return modTimes[cleanPath];
849- } catch (_) {
850- return null ;
788+ void _onEmbeddedCoverChanged () {
789+ if (! mounted || _embeddedCoverRefreshScheduled) return ;
790+ _embeddedCoverRefreshScheduled = true ;
791+ WidgetsBinding .instance.addPostFrameCallback ((_) {
792+ _embeddedCoverRefreshScheduled = false ;
793+ if (mounted) {
794+ setState (() {});
851795 }
852- }
853-
854- try {
855- final stat = await File (cleanPath).stat ();
856- return stat.modified.millisecondsSinceEpoch;
857- } catch (_) {
858- return null ;
859- }
796+ });
860797 }
861798
862799 Future <void > _scheduleDownloadedEmbeddedCoverRefreshForPath (
863800 String ? filePath, {
864801 int ? beforeModTime,
865802 bool force = false ,
866803 }) async {
867- final cleanPath = _cleanFilePath (filePath);
868- if (cleanPath.isEmpty) return ;
869-
870- if (! force) {
871- if (beforeModTime == null ) {
872- return ;
873- }
874- final afterModTime = await _readFileModTimeMillis (cleanPath);
875- if (afterModTime != null && afterModTime == beforeModTime) {
876- return ;
877- }
878- }
879-
880- _pendingDownloadedCoverRefresh.add (cleanPath);
881- _failedDownloadedCoverExtract.remove (cleanPath);
882- if (mounted) {
883- setState (() {});
884- }
804+ await DownloadedEmbeddedCoverResolver .scheduleRefreshForPath (
805+ filePath,
806+ beforeModTime: beforeModTime,
807+ force: force,
808+ onChanged: _onEmbeddedCoverChanged,
809+ );
885810 }
886811
887812 String ? _resolveDownloadedEmbeddedCoverPath (String ? filePath) {
888- final cleanPath = _cleanFilePath (filePath);
889- if (cleanPath.isEmpty) return null ;
890-
891- if (_pendingDownloadedCoverRefresh.remove (cleanPath)) {
892- _ensureDownloadedEmbeddedCover (cleanPath, forceRefresh: true );
893- }
894-
895- final cachedPath = _downloadedEmbeddedCoverCache[cleanPath];
896- if (cachedPath != null ) {
897- if (File (cachedPath).existsSync ()) {
898- return cachedPath;
899- }
900- _downloadedEmbeddedCoverCache.remove (cleanPath);
901- _cleanupTempCoverPathSync (cachedPath);
902- }
903-
904- return null ;
905- }
906-
907- void _ensureDownloadedEmbeddedCover (
908- String cleanPath, {
909- bool forceRefresh = false ,
910- }) {
911- if (cleanPath.isEmpty) return ;
912- if (_pendingDownloadedCoverExtract.contains (cleanPath)) return ;
913- if (! forceRefresh && _downloadedEmbeddedCoverCache.containsKey (cleanPath)) {
914- return ;
915- }
916- if (! forceRefresh && _failedDownloadedCoverExtract.contains (cleanPath)) {
917- return ;
918- }
919-
920- _pendingDownloadedCoverExtract.add (cleanPath);
921- Future .microtask (() async {
922- String ? outputPath;
923- try {
924- final tempDir = await Directory .systemTemp.createTemp ('library_cover_' );
925- outputPath = '${tempDir .path }${Platform .pathSeparator }cover.jpg' ;
926- final result = await PlatformBridge .extractCoverToFile (
927- cleanPath,
928- outputPath,
929- );
930-
931- final hasCover =
932- result['error' ] == null && await File (outputPath).exists ();
933- if (! hasCover) {
934- _failedDownloadedCoverExtract.add (cleanPath);
935- _cleanupTempCoverPathSync (outputPath);
936- return ;
937- }
938-
939- if (! mounted) {
940- _cleanupTempCoverPathSync (outputPath);
941- return ;
942- }
943-
944- final previous = _downloadedEmbeddedCoverCache[cleanPath];
945- _downloadedEmbeddedCoverCache[cleanPath] = outputPath;
946- _failedDownloadedCoverExtract.remove (cleanPath);
947- _trimDownloadedEmbeddedCoverCache ();
948- if (previous != null && previous != outputPath) {
949- _cleanupTempCoverPathSync (previous);
950- }
951- setState (() {});
952- } catch (_) {
953- _failedDownloadedCoverExtract.add (cleanPath);
954- _cleanupTempCoverPathSync (outputPath);
955- } finally {
956- _pendingDownloadedCoverExtract.remove (cleanPath);
957- }
958- });
813+ return DownloadedEmbeddedCoverResolver .resolve (
814+ filePath,
815+ onChanged: _onEmbeddedCoverChanged,
816+ );
959817 }
960818
961819 ValueListenable <bool > _fileExistsListenable (String ? filePath) {
0 commit comments