初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
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<String> 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<FileServiceResponse> get(String url, {Map<String, String>? 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<int> 或 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 = <String, String>{};
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;
}