修复hj加载
This commit is contained in:
parent
125b852590
commit
a122935747
@ -15,8 +15,8 @@ class MediaCache {
|
||||
final Duration stalePeriod;
|
||||
final int maxNrOfCacheObjects;
|
||||
|
||||
static String get videoCachePath => "$temporaryPath";
|
||||
static String get imageCachePath => "$temporaryPath";
|
||||
static String get videoCachePath => temporaryPath;
|
||||
static String get imageCachePath => temporaryPath;
|
||||
|
||||
MediaCache._internal({
|
||||
required this.cacheKey,
|
||||
@ -65,14 +65,24 @@ class MediaCache {
|
||||
/// 下载或获取缓存文件
|
||||
/// [url] 文件网络地址
|
||||
/// [ignoreCache] 是否忽略缓存强制重新下载
|
||||
Future<File> getFile({required String url, bool ignoreCache = false}) async {
|
||||
Future<File> getFile({
|
||||
required String url,
|
||||
bool ignoreCache = false,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
if (ignoreCache) {
|
||||
// 强制重新下载
|
||||
final fileInfo = await _cacheManager.downloadFile(url);
|
||||
final fileInfo = await _cacheManager.downloadFile(
|
||||
url,
|
||||
authHeaders: headers,
|
||||
);
|
||||
return fileInfo.file;
|
||||
} else {
|
||||
// 优先从缓存获取
|
||||
final cacheFile = await _cacheManager.getSingleFile(url);
|
||||
final cacheFile = await _cacheManager.getSingleFile(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
return cacheFile;
|
||||
}
|
||||
}
|
||||
@ -119,10 +129,8 @@ class MediaCache {
|
||||
localFile.readAsBytesSync(), // 文件字节数据
|
||||
fileExtension: localFile.path.split('.').last, // 文件扩展名
|
||||
);
|
||||
|
||||
// 2. 验证文件已缓存
|
||||
final cachedFile = await cacheManager.getFileFromCache(cacheKey);
|
||||
if (cachedFile != null) {}
|
||||
} catch (e) {}
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,6 +390,8 @@ class _RocketCanvas extends StatelessWidget {
|
||||
imageUrl: selectedLevelItem.previewImageUrl,
|
||||
fit: BoxFit.contain,
|
||||
preferSvg: true,
|
||||
pagDisplayScale: 2,
|
||||
pagClipBehavior: Clip.none,
|
||||
debugName:
|
||||
'centerPreview level=${selectedLevelItem.level} '
|
||||
'preview=${selectedLevelItem.previewImageUrl ?? '-'}',
|
||||
@ -974,6 +976,8 @@ class _RocketImage extends StatelessWidget {
|
||||
this.imageUrl,
|
||||
this.fit = BoxFit.cover,
|
||||
this.preferSvg = false,
|
||||
this.pagDisplayScale = 1,
|
||||
this.pagClipBehavior = Clip.hardEdge,
|
||||
this.debugName,
|
||||
});
|
||||
|
||||
@ -981,6 +985,8 @@ class _RocketImage extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final BoxFit fit;
|
||||
final bool preferSvg;
|
||||
final double pagDisplayScale;
|
||||
final Clip pagClipBehavior;
|
||||
final String? debugName;
|
||||
|
||||
@override
|
||||
@ -1005,6 +1011,8 @@ class _RocketImage extends StatelessWidget {
|
||||
resource: source,
|
||||
loop: true,
|
||||
fit: fit,
|
||||
displayScale: pagDisplayScale,
|
||||
clipBehavior: pagClipBehavior,
|
||||
underlayBuilder: (_) => _imageFallback(),
|
||||
defaultBuilder: (_) => const SCRotatingDotsLoading(),
|
||||
);
|
||||
@ -1054,6 +1062,8 @@ class _RocketImage extends StatelessWidget {
|
||||
resource: source,
|
||||
loop: true,
|
||||
fit: fit,
|
||||
displayScale: pagDisplayScale,
|
||||
clipBehavior: pagClipBehavior,
|
||||
underlayBuilder: (_) => _imageFallback(),
|
||||
defaultBuilder: (_) => _imageFallback(),
|
||||
);
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pag/pag.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
|
||||
const Duration _pagMemoryTtl = Duration(minutes: 5);
|
||||
|
||||
class RoomRocketPagEffectOverlay extends StatefulWidget {
|
||||
const RoomRocketPagEffectOverlay({
|
||||
super.key,
|
||||
@ -71,6 +76,9 @@ class _RoomRocketPagEffectOverlayState
|
||||
height: size.height,
|
||||
loop: false,
|
||||
fit: BoxFit.cover,
|
||||
displayScale: 1,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
memoryTtl: _pagMemoryTtl,
|
||||
onCompleted: _complete,
|
||||
defaultBuilder:
|
||||
(_) => const Center(child: SCRotatingDotsLoading()),
|
||||
@ -134,6 +142,9 @@ class RoomRocketPagPreview extends StatelessWidget {
|
||||
this.height,
|
||||
this.loop = true,
|
||||
this.fit = BoxFit.contain,
|
||||
this.displayScale = 1,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.memoryTtl = _pagMemoryTtl,
|
||||
this.underlayBuilder,
|
||||
this.defaultBuilder,
|
||||
});
|
||||
@ -143,6 +154,9 @@ class RoomRocketPagPreview extends StatelessWidget {
|
||||
final double? height;
|
||||
final bool loop;
|
||||
final BoxFit fit;
|
||||
final double displayScale;
|
||||
final Clip clipBehavior;
|
||||
final Duration memoryTtl;
|
||||
final WidgetBuilder? underlayBuilder;
|
||||
final WidgetBuilder? defaultBuilder;
|
||||
|
||||
@ -162,6 +176,9 @@ class RoomRocketPagPreview extends StatelessWidget {
|
||||
height: resolvedHeight,
|
||||
loop: loop,
|
||||
fit: fit,
|
||||
displayScale: displayScale,
|
||||
clipBehavior: clipBehavior,
|
||||
memoryTtl: memoryTtl,
|
||||
underlayBuilder: underlayBuilder,
|
||||
defaultBuilder:
|
||||
defaultBuilder ??
|
||||
@ -179,6 +196,9 @@ class _RoomRocketPagView extends StatefulWidget {
|
||||
this.height,
|
||||
required this.loop,
|
||||
required this.fit,
|
||||
required this.displayScale,
|
||||
required this.clipBehavior,
|
||||
required this.memoryTtl,
|
||||
this.underlayBuilder,
|
||||
this.onCompleted,
|
||||
this.defaultBuilder,
|
||||
@ -189,6 +209,9 @@ class _RoomRocketPagView extends StatefulWidget {
|
||||
final double? height;
|
||||
final bool loop;
|
||||
final BoxFit fit;
|
||||
final double displayScale;
|
||||
final Clip clipBehavior;
|
||||
final Duration memoryTtl;
|
||||
final WidgetBuilder? underlayBuilder;
|
||||
final VoidCallback? onCompleted;
|
||||
final WidgetBuilder? defaultBuilder;
|
||||
@ -205,7 +228,9 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
Uint8List? _pagBytes;
|
||||
bool _initialized = false;
|
||||
bool _loadFailed = false;
|
||||
bool _releasedFromMemory = false;
|
||||
Timer? _startWatchdog;
|
||||
Timer? _memoryReleaseTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -223,6 +248,8 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
_pagBytes = null;
|
||||
_initialized = false;
|
||||
_loadFailed = false;
|
||||
_releasedFromMemory = false;
|
||||
_memoryReleaseTimer?.cancel();
|
||||
_debug(
|
||||
'resource changed old=${_shortPagResource(oldWidget.resource)} '
|
||||
'new=${_shortPagResource(widget.resource)}',
|
||||
@ -235,9 +262,11 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
void dispose() {
|
||||
_debug(
|
||||
'dispose initialized=$_initialized '
|
||||
'hasBytes=${_pagBytes != null} loadFailed=$_loadFailed',
|
||||
'hasBytes=${_pagBytes != null} loadFailed=$_loadFailed '
|
||||
'released=$_releasedFromMemory',
|
||||
);
|
||||
_startWatchdog?.cancel();
|
||||
_memoryReleaseTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -253,6 +282,9 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
widget.defaultBuilder?.call(context) ??
|
||||
const Center(child: SCRotatingDotsLoading());
|
||||
final underlay = widget.underlayBuilder?.call(context);
|
||||
if (_releasedFromMemory) {
|
||||
return _releasedPlaceholder(underlay, defaultChild);
|
||||
}
|
||||
if (_loadFailed) {
|
||||
return _withUnderlay(underlay, defaultChild);
|
||||
}
|
||||
@ -274,24 +306,41 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
onAnimationRepeat: _handleAnimationRepeat,
|
||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||
);
|
||||
final visibleUnderlay = _initialized ? null : underlay;
|
||||
final content = Stack(
|
||||
fit: StackFit.expand,
|
||||
clipBehavior:
|
||||
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
||||
children: [
|
||||
if (visibleUnderlay != null) visibleUnderlay,
|
||||
_scaledPagView(pagView),
|
||||
if (!_initialized) Positioned.fill(child: defaultChild),
|
||||
],
|
||||
);
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: ClipRect(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (underlay != null) underlay,
|
||||
FittedBox(fit: widget.fit, child: pagView),
|
||||
if (!_initialized) Positioned.fill(child: defaultChild),
|
||||
],
|
||||
),
|
||||
),
|
||||
child:
|
||||
widget.clipBehavior == Clip.none ? content : ClipRect(child: content),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _scaledPagView(Widget pagView) {
|
||||
final scale =
|
||||
widget.displayScale.isFinite && widget.displayScale > 0
|
||||
? widget.displayScale
|
||||
: 1.0;
|
||||
final fitted = FittedBox(fit: widget.fit, child: pagView);
|
||||
if (scale == 1.0) {
|
||||
return fitted;
|
||||
}
|
||||
return Transform.scale(scale: scale, child: fitted);
|
||||
}
|
||||
|
||||
void _prepareResource() {
|
||||
final path = widget.resource.trim();
|
||||
_releasedFromMemory = false;
|
||||
_memoryReleaseTimer?.cancel();
|
||||
_debug(
|
||||
'prepare kind=${_pagSourceKind(path)} '
|
||||
'isPag=${RoomRocketPagEffectOverlay.isPag(path)}',
|
||||
@ -303,18 +352,13 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
Future<void> _loadBytes(String path) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
final isNetwork = _isPagNetwork(path);
|
||||
_debug(
|
||||
'${isNetwork ? 'download' : 'asset load'} start '
|
||||
'${_pagLoadAction(path)} start '
|
||||
'host=${_pagHost(path)}',
|
||||
);
|
||||
final data = await (isNetwork
|
||||
? NetworkAssetBundle(Uri.parse(path)).load(path)
|
||||
: rootBundle.load(path))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
final bytes = Uint8List.fromList(
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
|
||||
);
|
||||
final bytes = await _readPagBytes(
|
||||
path,
|
||||
).timeout(const Duration(seconds: 15));
|
||||
if (!mounted || widget.resource.trim() != path) {
|
||||
_debug(
|
||||
'bytes ignored stale mounted=$mounted '
|
||||
@ -323,7 +367,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
return;
|
||||
}
|
||||
if (bytes.isEmpty) {
|
||||
_markLoadFailed('download empty');
|
||||
_markLoadFailed('pag bytes empty');
|
||||
return;
|
||||
}
|
||||
_debug(
|
||||
@ -333,7 +377,9 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
);
|
||||
setState(() {
|
||||
_pagBytes = bytes;
|
||||
_releasedFromMemory = false;
|
||||
});
|
||||
_scheduleMemoryRelease(path);
|
||||
_restartWatchdog();
|
||||
} catch (error) {
|
||||
if (!mounted || widget.resource.trim() != path) {
|
||||
@ -445,6 +491,26 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleMemoryRelease(String path) {
|
||||
_memoryReleaseTimer?.cancel();
|
||||
if (widget.memoryTtl <= Duration.zero) {
|
||||
return;
|
||||
}
|
||||
_memoryReleaseTimer = Timer(widget.memoryTtl, () {
|
||||
if (!mounted || widget.resource.trim() != path || _pagBytes == null) {
|
||||
return;
|
||||
}
|
||||
_debug('release bytes from memory ttl=${widget.memoryTtl.inSeconds}s');
|
||||
_startWatchdog?.cancel();
|
||||
setState(() {
|
||||
_pagKey = GlobalKey<PAGViewState>();
|
||||
_pagBytes = null;
|
||||
_initialized = false;
|
||||
_releasedFromMemory = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _withUnderlay(Widget? underlay, Widget foreground) {
|
||||
if (underlay == null) {
|
||||
return foreground;
|
||||
@ -459,9 +525,18 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _releasedPlaceholder(Widget? underlay, Widget fallback) {
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: underlay ?? fallback,
|
||||
);
|
||||
}
|
||||
|
||||
void _markLoadFailed(String reason) {
|
||||
_debug('failed reason=$reason');
|
||||
_startWatchdog?.cancel();
|
||||
_memoryReleaseTimer?.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_initialized = false;
|
||||
@ -485,23 +560,76 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
'size=${widget.width?.toStringAsFixed(1)}x'
|
||||
'${widget.height?.toStringAsFixed(1)} '
|
||||
'kind=${_pagSourceKind(widget.resource)} '
|
||||
'scale=${widget.displayScale.toStringAsFixed(2)} '
|
||||
'released=$_releasedFromMemory '
|
||||
'resource=${_shortPagResource(widget.resource)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Duration debugRoomRocketPagMemoryTtl() {
|
||||
return _pagMemoryTtl;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
String debugNormalizeRoomRocketPagResource(String resource) {
|
||||
return _normalizePagResource(resource);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Uri? debugRoomRocketPagNetworkUri(String resource) {
|
||||
return _pagNetworkUri(resource);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
String? debugRoomRocketPagFilePath(String resource) {
|
||||
return _pagFilePath(resource);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
String debugRoomRocketPagSourceKind(String resource) {
|
||||
return _pagSourceKind(resource);
|
||||
}
|
||||
|
||||
Future<Uint8List> _readPagBytes(String value) async {
|
||||
final normalized = _normalizePagResource(value);
|
||||
final uri = _pagNetworkUri(normalized);
|
||||
if (uri != null) {
|
||||
final url = uri.toString();
|
||||
final file = await FileCacheManager.getInstance().getFile(
|
||||
url: url,
|
||||
headers: buildNetworkImageHeaders(url),
|
||||
);
|
||||
return file.readAsBytes();
|
||||
}
|
||||
|
||||
final filePath = _pagFilePath(normalized);
|
||||
if (filePath != null) {
|
||||
return File(filePath).readAsBytes();
|
||||
}
|
||||
|
||||
final data = await rootBundle.load(normalized);
|
||||
return Uint8List.fromList(
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isPagNetwork(String value) {
|
||||
return value.startsWith('http://') || value.startsWith('https://');
|
||||
return _pagNetworkUri(value) != null;
|
||||
}
|
||||
|
||||
String _pagSourceKind(String value) {
|
||||
final text = value.trim();
|
||||
final text = _normalizePagResource(value);
|
||||
if (text.isEmpty) {
|
||||
return 'empty';
|
||||
}
|
||||
if (_isPagNetwork(text)) {
|
||||
return 'network';
|
||||
}
|
||||
if (_pagFilePath(text) != null) {
|
||||
return 'file';
|
||||
}
|
||||
if (text.startsWith('sc_images/') || text.startsWith('assets/')) {
|
||||
return 'asset';
|
||||
}
|
||||
@ -509,10 +637,67 @@ String _pagSourceKind(String value) {
|
||||
}
|
||||
|
||||
String _pagHost(String value) {
|
||||
if (!_isPagNetwork(value)) {
|
||||
return '-';
|
||||
return _pagNetworkUri(value)?.host ?? '-';
|
||||
}
|
||||
|
||||
String _pagLoadAction(String value) {
|
||||
return switch (_pagSourceKind(value)) {
|
||||
'network' => 'download',
|
||||
'file' => 'file load',
|
||||
'asset' => 'asset load',
|
||||
_ => 'asset load',
|
||||
};
|
||||
}
|
||||
|
||||
String _normalizePagResource(String value) {
|
||||
final normalized = normalizeImageResourceUrl(value.trim());
|
||||
return _pagNetworkUri(normalized)?.toString() ?? normalized;
|
||||
}
|
||||
|
||||
Uri? _pagNetworkUri(String value) {
|
||||
final text = normalizeImageResourceUrl(value.trim());
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Uri.tryParse(value)?.host ?? '-';
|
||||
final direct = Uri.tryParse(text);
|
||||
if (_isHttpUri(direct)) {
|
||||
return direct;
|
||||
}
|
||||
final encoded = Uri.tryParse(Uri.encodeFull(text));
|
||||
if (_isHttpUri(encoded)) {
|
||||
return encoded;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isHttpUri(Uri? uri) {
|
||||
if (uri == null || !uri.hasScheme || uri.host.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final scheme = uri.scheme.toLowerCase();
|
||||
return scheme == 'http' || scheme == 'https';
|
||||
}
|
||||
|
||||
String? _pagFilePath(String value) {
|
||||
final text = value.trim();
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final uri = Uri.tryParse(text);
|
||||
if (uri != null && uri.scheme.toLowerCase() == 'file') {
|
||||
try {
|
||||
return uri.toFilePath();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (text.startsWith('/')) {
|
||||
return text;
|
||||
}
|
||||
if (RegExp(r'^[A-Za-z]:[\\/]').hasMatch(text)) {
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _shortPagResource(String value) {
|
||||
|
||||
48
test/room_rocket_pag_resource_test.dart
Normal file
48
test/room_rocket_pag_resource_test.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/app/config/app_config.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
AppConfig.initialize();
|
||||
});
|
||||
|
||||
test('normalizes unescaped pag network urls', () {
|
||||
final uri = debugRoomRocketPagNetworkUri(
|
||||
'https://example.com/assets/火箭 输出/火箭1级.pag?name=火箭 1',
|
||||
);
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri!.scheme, 'https');
|
||||
expect(uri.host, 'example.com');
|
||||
expect(uri.toString(), contains('%E7%81%AB%E7%AE%AD'));
|
||||
expect(uri.toString(), contains('%20'));
|
||||
});
|
||||
|
||||
test('normalizes protocol-relative and server-relative pag urls', () {
|
||||
final apiUri = Uri.parse(SCGlobalConfig.apiHost);
|
||||
final apiScheme = apiUri.scheme == 'http' ? 'http' : 'https';
|
||||
expect(
|
||||
debugNormalizeRoomRocketPagResource('//cdn.example.com/rocket.pag'),
|
||||
'$apiScheme://cdn.example.com/rocket.pag',
|
||||
);
|
||||
expect(
|
||||
debugNormalizeRoomRocketPagResource('/external/oss/local/火箭.pag'),
|
||||
apiUri.resolve('/external/oss/local/火箭.pag').toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test('detects local file sources before falling back to assets', () {
|
||||
expect(
|
||||
debugRoomRocketPagFilePath('file:///tmp/%E7%81%AB%E7%AE%AD.pag'),
|
||||
'/tmp/火箭.pag',
|
||||
);
|
||||
expect(debugRoomRocketPagSourceKind('/tmp/rocket.pag'), 'file');
|
||||
expect(debugRoomRocketPagSourceKind('assets/rocket/rocket.pag'), 'asset');
|
||||
});
|
||||
|
||||
test('keeps pag bytes in memory for five minutes by default', () {
|
||||
expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5));
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user