import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:logging/logging.dart'; import 'package:puzzleweave/models/api_helper.dart'; import 'package:puzzleweave/models/items.dart'; import 'package:puzzleweave/utils/utils.dart'; import 'package:puzzleweave/utils/image_decoder.dart'; import 'data.dart'; final Logger _log = Logger('download.dart'); /// ✅ 优化:增加缓存数量到3个 const maxCachedItems = 3; /// Singleton class Download { static final Download _instance = Download._internal(); factory Download() => _instance; Download._internal(); final Map _cache = {}; DownloadItem download(String url, String cachePath) { if (_cache[url] != null) { _log.info('Cache hit for $url'); _cache[url]!.touch(); return _cache[url]!; } else { final item = DownloadItem(url, cachePath); _cache[url] = item; _watch(item); return item; } } _watch(DownloadItem item) async { try { await item.loadCompleter.future; Future.microtask(_clean); } catch (err) { _log.info('Watch download item got error: $err'); _cache.remove(item.url); } } _clean() { final list = _cache.values.where((item) => item.loadCompleter.isCompleted).toList(); if (list.length <= maxCachedItems) return; _log.info('Cleaning download cache...'); list.sort((a, b) => a.lastUsed.compareTo(b.lastUsed)); while (list.length > maxCachedItems) { final item = list.removeAt(0); _log.info('Cleaning item from memory: $item'); item.dispose(); _cache.remove(item.url); } } /// ✅ 新增:清理所有内存中的数据(保留下载任务) void clearAllData() { _log.info('Clearing all download data from memory'); int count = 0; _cache.forEach((key, item) { if (item.data != null) { item.clearData(); count++; } }); _log.info('Cleared $count download items from memory'); } /// ✅ 新增:获取当前缓存统计 Map getCacheStats() { final total = _cache.length; final completed = _cache.values.where((item) => item.loadCompleter.isCompleted).length; final withData = _cache.values.where((item) => item.data != null).length; return {'total': total, 'completed': completed, 'withData': withData}; } clearAllCached() async { final file = await localFile('cache'); await file.delete(recursive: true); } } class DownloadItem { final String url; final String cachePath; ValueNotifier progress = ValueNotifier(0.0); final Completer loadCompleter = Completer(); Client? client; StreamSubscription? subscription; /// ✅ 新增:最后访问时间 DateTime? _lastAccessTime; /// ✅ 新增:数据保留时长(5分钟未使用则自动清理) static const _dataRetentionDuration = Duration(minutes: 5); /// ✅ 新增:自动清理定时器 Timer? _cleanupTimer; int lastUsed; Uint8List? _data; bool _isDisposed = false; DownloadItem(this.url, this.cachePath) : lastUsed = DateTime.now().millisecondsSinceEpoch { _log.info('New download item for: $url'); _start(); } Uint8List? get data => _data; set data(Uint8List? val) => _data = val; touch() { lastUsed = DateTime.now().millisecondsSinceEpoch; _lastAccessTime = DateTime.now(); } _start() async { try { await _download(); if (!loadCompleter.isCompleted) loadCompleter.complete(); } catch (err) { if (!loadCompleter.isCompleted) loadCompleter.completeError(err); } } Future _download() async { progress.value = 0; final file = await localFile(cachePath); _checkDispose(); if (await file.exists()) { _log.info('Disk cache hit (Metadata only) for $cachePath'); progress.value = 1.0; return; } final List bytes = []; client = Client(); final response = await client!.send(Request('GET', Uri.parse(url))); _checkDispose(); if (response.statusCode != 200) throw Exception('Status:${response.statusCode}'); final length = response.contentLength ?? 0; final streamCompleter = Completer(); subscription = response.stream.listen( (value) { bytes.addAll(value); if (length > 0) progress.value = bytes.length / length; }, onDone: () => streamCompleter.complete(), onError: (e) => streamCompleter.completeError(e), cancelOnError: true, ); await streamCompleter.future; _checkDispose(); if (bytes.length < 1024 + 24) { throw Exception('Downloaded data is too small (${bytes.length} bytes)'); } final cleanBytes = Uint8List.fromList(bytes.sublist(24)); _data = cleanBytes; /// ✅ 原子性写入优化 final tempPath = '$cachePath.tmp'; await saveBytes(tempPath, _data!); final tempFile = await localFile(tempPath); final finalFile = await localFile(cachePath); await tempFile.rename(finalFile.path); _log.info('Download and save complete for $url'); client?.close(); /// ✅ 调度自动清理 _scheduleCleanup(); } /// ✅ 供 Loader 真正需要数据时调用 Future ensureDataLoaded() async { _lastAccessTime = DateTime.now(); if (_data != null) { _log.info('Data cache hit for $cachePath'); _scheduleCleanup(); return _data!; } _log.info('Performing late read from disk: $cachePath'); final file = await localFile(cachePath); _data = await file.readAsBytes(); _scheduleCleanup(); return _data!; } /// ✅ 新增:调度自动清理 void _scheduleCleanup() { _cleanupTimer?.cancel(); _cleanupTimer = Timer(_dataRetentionDuration, () { if (_data != null && _lastAccessTime != null) { final timeSinceLastAccess = DateTime.now().difference(_lastAccessTime!); if (timeSinceLastAccess >= _dataRetentionDuration) { _log.info('Auto-cleaning unused data: $cachePath'); _data = null; } } }); } /// ✅ 新增:手动清理数据 void clearData() { _cleanupTimer?.cancel(); _data = null; _log.info('Manually cleared data: $cachePath'); } _checkDispose() { if (_isDisposed) throw Exception('Disposed'); } dispose() { _isDisposed = true; _cleanupTimer?.cancel(); _data = null; subscription?.cancel(); client?.close(); } @override String toString() => '[$cachePath]'; } abstract class ItemLoader { final Completer completer = Completer(); Uint8List? get data; ValueNotifier get progress; ItemLoader(); Future _prepareData() async { await completer.future; if (data == null) throw 'Data missing after completion'; return data!; } Future getImageBySize(int width, int height, {allowUpscaling = false}) async { final bytes = await _prepareData(); final codec = await ui.instantiateImageCodec(bytes, targetHeight: height, targetWidth: width, allowUpscaling: allowUpscaling); return (await codec.getNextFrame()).image; } Future getImage() async { final bytes = await _prepareData(); final codec = await ui.instantiateImageCodec(bytes); return (await codec.getNextFrame()).image; } static void preload(ListItem item, String quality) { if (BuiltinRegistry.contains(item.id)) { _log.info('Preload: Skipping builtin ${item.id}'); return; } if (item is RemoteItem) { Download().download(ApiHelper.imageUri(item.id, quality), item.cachePath); } } factory ItemLoader.load(ListItem item, String quality) { if (BuiltinRegistry.contains(item.id)) { _log.info('Built-in hit: ${item.id}, skip network.'); return AssetItemLoader('assets/builtin/${item.id}.jpeg'); } switch (item.runtimeType) { case RemoteItem: return RemoteItemLoader(ApiHelper.imageUri(item.id, quality), item.cachePath); case AssetItem: return AssetItemLoader((item as AssetItem).image); default: throw 'Unknown item type'; } } } class AssetItemLoader extends ItemLoader { final String path; Uint8List? _data; @override ValueNotifier progress = ValueNotifier(0); AssetItemLoader(this.path) { _load(); } _load() async { try { _data = await loadFileDataFromAsset(path); completer.complete(); progress.value = 1.0; } catch (e) { completer.completeError(e); } } @override Uint8List? get data => _data; } class RemoteItemLoader extends ItemLoader { final String url; final String cachePath; late final DownloadItem downloadItem; RemoteItemLoader(this.url, this.cachePath) { downloadItem = Download().download(url, cachePath); _load(); } _load() async { try { await downloadItem.loadCompleter.future; completer.complete(); } catch (err) { completer.completeError(err); } } @override Uint8List? get data => downloadItem.data; @override Future _prepareData() async { await completer.future; return await downloadItem.ensureDataLoaded(); } @override ValueNotifier get progress => downloadItem.progress; }