| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- 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<String, DownloadItem> _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<String, dynamic> 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<double> progress = ValueNotifier(0.0);
- final Completer<void> 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<void> _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<int> 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<Uint8List> 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<double> get progress;
- ItemLoader();
- Future<Uint8List> _prepareData() async {
- await completer.future;
- if (data == null) throw 'Data missing after completion';
- return data!;
- }
- Future<ui.Image> 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<ui.Image> 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<double> 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<Uint8List> _prepareData() async {
- await completer.future;
- return await downloadItem.ensureDataLoaded();
- }
- @override
- ValueNotifier<double> get progress => downloadItem.progress;
- }
|