Files
app/lib/services/thumbnail_service.dart

149 lines
4.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'api_service.dart';
import '../core/utils/app_logger.dart';
import '../core/utils/file_utils.dart';
import '../core/utils/time_flow_decoder.dart';
import 'custom_line_service.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<String, _ThumbCacheEntry> _urlCache = {};
// 请求去重: fileUri -> in-flight Future
final Map<String, Future<String?>> _inFlightRequests = {};
/// 获取缩略图图片 URL
/// [fileUri] 文件的 relativePath(如 /path/to/file.jpg
/// [contextHint] 可选,加速服务端 DB 查询
Future<String?> getThumbnailUrl({
required String fileUri,
String? contextHint,
}) async {
final cacheKey = fileUri;
// 1. 检查内存缓存
final cached = _urlCache[cacheKey];
if (cached != null && !cached.isExpired) {
await CustomLineService.instance.activateSelectedNodeForUrl(
cached.imageUrl,
);
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<String?> _fetchThumbnailUrl(
String fileUri,
String? contextHint,
) async {
try {
final uri = FileUtils.toCloudreveUri(fileUri);
final headers = contextHint != null
? <String, dynamic>{'X-Cr-Context-Hint': contextHint}
: null;
// _parseResponse 已提取 ApiResponse.dataresponse 即 {url, obfuscated, expires}
final response = await ApiService.instance.get<Map<String, dynamic>>(
'/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');
await CustomLineService.instance.activateSelectedNodeForUrl(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();
}
}