import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import '../../config/address.dart'; import '../../extension/extensions.dart'; import '../debug_log.dart'; import '../image/image_data_handle/image_crypto.dart'; import '../net/load_apk/dio_cli.dart'; class ImageCacheManager extends CacheManager { static const image_key = "customCache"; // 工厂模式 factory ImageCacheManager() => _getInstance(); static ImageCacheManager get instance => _getInstance(); static ImageCacheManager? _instance; static ImageCacheManager _getInstance() { _instance ??= ImageCacheManager._internal(); return _instance!; } ImageCacheManager._internal() : super(Config(image_key, maxNrOfCacheObjects: 400, stalePeriod: const Duration(days: 7), fileService: CustomFileRespons())); Future getFilePath() async { var directory = await getTemporaryDirectory(); return path.join(directory.path, image_key); } } class CustomFileRespons extends HttpFileService { DioCli client = DioCli( options: BaseOptions( connectTimeout: const Duration(milliseconds: 30000), receiveTimeout: const Duration(milliseconds: 30000), sendTimeout: const Duration(milliseconds: 30000), validateStatus: (int? status) { if (status != null) return status < 600; return false; })); @override Future get(String url, {Map? headers = const {}}) async { if (!url.startsWith("http") && !url.startsWith("https")) { url = path.join(Address.baseImagePath ?? '', url); } if (kDebugMode) { debugLog("image_request", "get()...begin...ulr:$url..."); } headers?['cache-control'] = 'max-age=31104000'; final resp = await client.getBytes(url, headers: headers); final statusCode = resp.data?.statusCode ?? HttpStatus.badRequest; // 失败 / 非 2xx:直接抛。避免错误响应(空内容、CDN 错误页)被落盘缓存成坏图, // 让 CachedNetworkImage 走 errorWidget;下次能重新请求,而不是一直命中坏缓存。 if (resp.err != null || statusCode < 200 || statusCode >= 300) { if (kDebugMode) { debugLog("image_request", "get()...failed...$url...code:($statusCode): ${resp.err}"); } throw HttpException("image fetch failed ($statusCode): ${resp.err}", uri: Uri.tryParse(url)); } // 解密(Dio 不同版本 bytes 返回 List 或 Uint8List)。解密失败 / 空字节同样抛出,不缓存坏图。 Uint8List bytes; try { final raw = resp.data?.data; final decrypted = raw == null ? null : ImageCrypto.decryptImage(raw is Uint8List ? raw : Uint8List.fromList(raw)); if (decrypted == null || decrypted.isEmpty) { throw const FormatException("empty image bytes"); } //部分 CDN 返回的加密 JPEG 缺末尾 EOI(FF D9),Skia 严格 decoder 会拒;补 EOI 让标准 decoder 也能解 bytes = _repairJpegEoiIfNeeded(decrypted); } catch (e) { if (kDebugMode) debugLog("ImageCacheManager", "decrypt failed: $url -> $e"); throw HttpException("image decrypt failed: $e", uri: Uri.tryParse(url)); } final respHeaders = {}; resp.data?.headers.forEach((key, arrayValue) { if (arrayValue.notEmpty()) respHeaders[key] = arrayValue[0]; }); if (kDebugMode) { debugLog("image_request", "get()...success...$url...code:($statusCode): contentLength:${bytes.length}"); } return HttpGetResponse(StreamedResponse( Stream.value(bytes), statusCode, contentLength: bytes.length, // 补 EOI 后用真实长度 headers: respHeaders, )); } } //判断是 jpeg 头但无 FF D9 结尾时补 EOI;其他格式 / 已完整 jpeg 原样返回 Uint8List _repairJpegEoiIfNeeded(Uint8List bytes) { if (bytes.length < 4) return bytes; final isJpeg = bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF; if (!isJpeg) return bytes; final hasEoi = bytes[bytes.length - 2] == 0xFF && bytes[bytes.length - 1] == 0xD9; if (hasEoi) return bytes; final fixed = Uint8List(bytes.length + 2) ..setRange(0, bytes.length, bytes) ..[bytes.length] = 0xFF ..[bytes.length + 1] = 0xD9; return fixed; }