diff --git a/lib/services/cover_cache_manager.dart b/lib/services/cover_cache_manager.dart index 92033e96..81347d11 100644 --- a/lib/services/cover_cache_manager.dart +++ b/lib/services/cover_cache_manager.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -14,6 +15,11 @@ class CoverCacheManager { static const String _cacheKey = 'coverImageCache'; static const int _maxCacheObjects = 1000; static const Duration _maxCacheAge = Duration(days: 365); + // flutter_cache_manager only caps object count, not bytes; hi-res covers + // run 300KB-1.5MB each, so 1000 objects can mean hundreds of MB on a + // storage-starved device. Sweep oldest files past the byte cap on init. + static const int _maxCacheBytes = 150 << 20; + static const int _sweepTargetBytes = 120 << 20; static CacheManager? _instance; static bool _initialized = false; @@ -44,11 +50,49 @@ class CoverCacheManager { _initialized = true; debugPrint('CoverCacheManager: Initialized successfully'); + unawaited(_sweepOverByteCap(_cachePath!)); } catch (e) { debugPrint('CoverCacheManager: Failed to initialize: $e'); } } + /// Deletes oldest cover files until the cache is back under + /// [_sweepTargetBytes]. Stale JSON repo entries self-heal: a missing file + /// is a cache miss and gets re-downloaded on demand. + static Future _sweepOverByteCap(String cachePath) async { + try { + final dir = Directory(cachePath); + if (!await dir.exists()) return; + + final files = []; + var totalSize = 0; + await for (final entity in dir.list(recursive: true)) { + if (entity is File && !entity.path.endsWith('.json')) { + files.add(entity); + totalSize += await entity.length(); + } + } + if (totalSize <= _maxCacheBytes) return; + + final stats = { + for (final file in files) file: await file.stat(), + }; + files.sort( + (a, b) => stats[a]!.modified.compareTo(stats[b]!.modified), + ); + for (final file in files) { + if (totalSize <= _sweepTargetBytes) break; + try { + totalSize -= stats[file]!.size; + await file.delete(); + } catch (_) {} + } + debugPrint('CoverCacheManager: Swept cover cache over byte cap'); + } catch (e) { + debugPrint('CoverCacheManager: Byte-cap sweep failed: $e'); + } + } + static Future clearCache() async { if (!_initialized || _instance == null || _cachePath == null) { await initialize();