import 'api_service.dart'; import '../core/utils/app_logger.dart'; import '../core/utils/file_utils.dart'; import '../core/utils/time_flow_decoder.dart'; /// 缩略图缓存条目 class _ThumbCacheEntry { final String imageUrl; final DateTime expiresAt; _ThumbCacheEntry({required this.imageUrl, required this.expiresAt}); bool get isExpired => DateTime.now().isAfter(expiresAt); } /// 缩略图服务 — 获取、解码、缓存缩略图 URL class ThumbnailService { static ThumbnailService? _instance; static ThumbnailService get instance { _instance ??= ThumbnailService._(); return _instance!; } ThumbnailService._(); // 内存缓存: fileUri -> cache entry final Map _urlCache = {}; // 请求去重: fileUri -> in-flight Future final Map> _inFlightRequests = {}; /// 获取缩略图图片 URL /// [fileUri] 文件的 relativePath(如 /path/to/file.jpg) /// [contextHint] 可选,加速服务端 DB 查询 Future getThumbnailUrl({ required String fileUri, String? contextHint, }) async { final cacheKey = fileUri; // 1. 检查内存缓存 final cached = _urlCache[cacheKey]; if (cached != null && !cached.isExpired) { return cached.imageUrl; } if (cached != null) { _urlCache.remove(cacheKey); } // 2. 检查请求去重 final inFlight = _inFlightRequests[cacheKey]; if (inFlight != null) { return inFlight; } // 3. 发起请求 final future = _fetchThumbnailUrl(fileUri, contextHint); _inFlightRequests[cacheKey] = future; try { return await future; } finally { _inFlightRequests.remove(cacheKey); } } Future _fetchThumbnailUrl(String fileUri, String? contextHint) async { try { final uri = FileUtils.toCloudreveUri(fileUri); final headers = contextHint != null ? {'X-Cr-Context-Hint': contextHint} : null; // _parseResponse 已提取 ApiResponse.data,response 即 {url, obfuscated, expires} final response = await ApiService.instance.get>( '/file/thumb', queryParameters: {'uri': uri}, headers: headers, ); AppLogger.d('ThumbnailService: response for $fileUri = $response'); var url = response['url'] as String? ?? ''; final obfuscated = response['obfuscated'] as bool? ?? false; final expiresStr = response['expires'] as String?; // 解码混淆 URL if (obfuscated && url.isNotEmpty) { final decoded = TimeFlowDecoder.decodeTimeFlowString(url); if (decoded == null) { AppLogger.w('Failed to decode obfuscated thumbnail URL for $fileUri'); return null; } url = decoded; } if (url.isEmpty) return null; AppLogger.d('ThumbnailService: resolved URL for $fileUri = $url'); // 解析过期时间,缓存提前 30 秒过期 DateTime expiresAt; if (expiresStr != null) { try { expiresAt = DateTime.parse(expiresStr).subtract(const Duration(seconds: 30)); } catch (_) { expiresAt = DateTime.now().add(const Duration(minutes: 5)); } } else { expiresAt = DateTime.now().add(const Duration(minutes: 5)); } // 存入内存缓存 _urlCache[fileUri] = _ThumbCacheEntry( imageUrl: url, expiresAt: expiresAt, ); return url; } catch (e) { AppLogger.d('ThumbnailService: failed to get thumbnail URL for $fileUri: $e'); return null; } } /// 移除指定文件的缓存 URL void evictUrl(String fileUri) { _urlCache.remove(fileUri); } /// 清空所有缓存(目录切换时调用) void clearAll() { _urlCache.clear(); _inFlightRequests.clear(); } }