yumi-flutter/lib/shared/tools/sc_network_image_utils.dart
2026-05-08 22:52:12 +08:00

161 lines
4.8 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/painting.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
bool _requiresAuthenticatedImageHeaders(String url) {
return url.contains("/external/oss/local/");
}
Map<String, String>? buildNetworkImageHeaders(String url) {
if (!_requiresAuthenticatedImageHeaders(url)) {
return null;
}
final uri = Uri.tryParse(url);
final headers = <String, String>{
"req-lang": SCGlobalConfig.lang,
"X-Forwarded-Proto": "http",
"User-Agent": "Dart/3.7.2(dart:io)",
"req-imei": SCGlobalConfig.imei,
"req-app-intel":
"version=${SCGlobalConfig.version};build=${SCGlobalConfig.build};model=${SCGlobalConfig.model};sysVersion=${SCGlobalConfig.sysVersion};channel=${SCGlobalConfig.channel}",
"req-client": Platform.isAndroid ? "Android" : "iOS",
"req-sys-origin":
"origin=${SCGlobalConfig.origin};originChild=${SCGlobalConfig.originChild}",
};
if ((uri?.host ?? "").isNotEmpty) {
headers["Host"] = uri!.host;
}
final token = AccountStorage().getToken();
if (token.isNotEmpty) {
headers["Authorization"] = "Bearer $token";
}
headers.removeWhere((key, value) => value.trim().isEmpty);
return headers;
}
String normalizeImageResourceUrl(String resource) {
final trimmed = resource.trim();
if (trimmed.startsWith('//')) {
final apiScheme = Uri.tryParse(SCGlobalConfig.apiHost)?.scheme;
final scheme = apiScheme == 'http' ? 'http' : 'https';
return '$scheme:$trimmed';
}
if (_isServerRelativeImagePath(trimmed)) {
final baseUri = Uri.tryParse(SCGlobalConfig.apiHost);
if (baseUri != null && baseUri.hasScheme && baseUri.host.isNotEmpty) {
final path = trimmed.startsWith('/') ? trimmed : '/$trimmed';
return baseUri.resolve(path).toString();
}
}
return trimmed;
}
bool _isServerRelativeImagePath(String value) {
final lowerValue = value.toLowerCase();
return lowerValue.startsWith('/external/') ||
lowerValue.startsWith('external/') ||
lowerValue.startsWith('/go/') ||
lowerValue.startsWith('go/') ||
lowerValue.startsWith('/static/') ||
lowerValue.startsWith('static/') ||
lowerValue.startsWith('/upload') ||
lowerValue.startsWith('upload');
}
int? _resolveImageProviderCacheDimension(double? logicalSize) {
if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) {
return null;
}
final devicePixelRatio =
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 2.0;
final maxDimension = SCGlobalConfig.isLowPerformanceDevice ? 1280 : 1920;
final pixelSize = (logicalSize * devicePixelRatio).round();
return pixelSize.clamp(1, maxDimension).toInt();
}
ImageProvider<Object> buildCachedImageProvider(
String url, {
double? logicalWidth,
double? logicalHeight,
}) {
final normalizedUrl = normalizeImageResourceUrl(url);
late final ImageProvider<Object> provider;
if (normalizedUrl.startsWith("http")) {
provider = ExtendedNetworkImageProvider(
normalizedUrl,
headers: buildNetworkImageHeaders(normalizedUrl),
cache: true,
cacheMaxAge: const Duration(days: 7),
);
} else if (normalizedUrl.startsWith('/')) {
provider = FileImage(File(normalizedUrl));
} else {
provider = AssetImage(normalizedUrl);
}
return ResizeImage.resizeIfNeeded(
_resolveImageProviderCacheDimension(logicalWidth),
_resolveImageProviderCacheDimension(logicalHeight),
provider,
);
}
Future<bool> warmImageResource(
String resource, {
double? logicalWidth,
double? logicalHeight,
Duration timeout = const Duration(seconds: 6),
}) async {
if (resource.isEmpty) {
return false;
}
final provider = buildCachedImageProvider(
resource,
logicalWidth: logicalWidth,
logicalHeight: logicalHeight,
);
final stream = provider.resolve(ImageConfiguration.empty);
final completer = Completer<bool>();
late final ImageStreamListener listener;
listener = ImageStreamListener(
(image, synchronousCall) {
if (!completer.isCompleted) {
completer.complete(true);
}
stream.removeListener(listener);
},
onError: (Object error, StackTrace? stackTrace) {
if (!completer.isCompleted) {
completer.complete(false);
}
stream.removeListener(listener);
},
);
stream.addListener(listener);
return completer.future.timeout(
timeout,
onTimeout: () {
stream.removeListener(listener);
return false;
},
);
}
Future<bool> warmNetworkImage(
String url, {
double? logicalWidth,
double? logicalHeight,
Duration timeout = const Duration(seconds: 6),
}) {
return warmImageResource(
url,
logicalWidth: logicalWidth,
logicalHeight: logicalHeight,
timeout: timeout,
);
}