404 lines
11 KiB
Dart
404 lines
11 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:extended_image/extended_image.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
|
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
|
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
|
|
|
const Duration _roomEmojiGifRestartInterval = Duration(milliseconds: 3500);
|
|
|
|
class RoomEmojiAssetImage extends StatefulWidget {
|
|
const RoomEmojiAssetImage({
|
|
super.key,
|
|
required this.asset,
|
|
required this.width,
|
|
required this.height,
|
|
this.fit = BoxFit.contain,
|
|
});
|
|
|
|
final String asset;
|
|
final double width;
|
|
final double height;
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
State<RoomEmojiAssetImage> createState() => _RoomEmojiAssetImageState();
|
|
}
|
|
|
|
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|
with AutomaticKeepAliveClientMixin {
|
|
static final Set<String> _loggedNetworkLoading = <String>{};
|
|
static final Set<String> _loggedNetworkReady = <String>{};
|
|
static final Set<String> _loggedNetworkError = <String>{};
|
|
|
|
AssetImage? _imageProvider;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_syncImageProvider();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_precache();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.asset != widget.asset) {
|
|
_syncImageProvider();
|
|
_precache();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final resource = widget.asset.trim();
|
|
if (resource.isEmpty) {
|
|
return SizedBox(width: widget.width, height: widget.height);
|
|
}
|
|
if (SCNetworkSvgaWidget.isSvga(resource)) {
|
|
return RepaintBoundary(
|
|
child: SCNetworkSvgaWidget(
|
|
resource: resource,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
loop: true,
|
|
fit: widget.fit,
|
|
fallback: _buildNetworkFallback(resource, 'svga fallback'),
|
|
),
|
|
);
|
|
}
|
|
if (_isGif(resource)) {
|
|
return RepaintBoundary(
|
|
child: _RestartingGifImage(
|
|
resource: resource,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
isLocalAsset: _isLocalAsset(resource),
|
|
),
|
|
);
|
|
}
|
|
if (!_isLocalAsset(resource)) {
|
|
return RepaintBoundary(child: _buildNetworkImage(resource));
|
|
}
|
|
|
|
final imageProvider = _imageProvider;
|
|
if (imageProvider == null) {
|
|
return SizedBox(width: widget.width, height: widget.height);
|
|
}
|
|
return RepaintBoundary(
|
|
child: Image(
|
|
image: imageProvider,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
gaplessPlayback: true,
|
|
filterQuality: FilterQuality.low,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _syncImageProvider() {
|
|
final resource = widget.asset.trim();
|
|
_imageProvider = _isLocalAsset(resource) ? AssetImage(resource) : null;
|
|
}
|
|
|
|
void _precache() {
|
|
final imageProvider = _imageProvider;
|
|
if (imageProvider == null) {
|
|
return;
|
|
}
|
|
precacheImage(imageProvider, context);
|
|
}
|
|
|
|
bool _isLocalAsset(String resource) {
|
|
return resource.startsWith("assets/") || resource.startsWith("sc_images/");
|
|
}
|
|
|
|
bool _isGif(String resource) {
|
|
final lower = resource.trim().toLowerCase();
|
|
return lower.endsWith('.gif') || lower.contains('.gif?');
|
|
}
|
|
|
|
Widget _buildNetworkImage(String resource) {
|
|
final normalizedUrl = normalizeImageResourceUrl(resource);
|
|
return ExtendedImage.network(
|
|
normalizedUrl,
|
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
|
cache: true,
|
|
clearMemoryCacheWhenDispose: false,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
gaplessPlayback: true,
|
|
loadStateChanged: (state) {
|
|
switch (state.extendedImageLoadState) {
|
|
case LoadState.loading:
|
|
_logNetworkOnce(_loggedNetworkLoading, 'loading', normalizedUrl);
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: const Center(child: SCRotatingDotsLoading()),
|
|
);
|
|
case LoadState.completed:
|
|
final imageInfo = state.extendedImageInfo;
|
|
final image = imageInfo?.image;
|
|
final size =
|
|
image == null ? 'unknown' : '${image.width}x${image.height}';
|
|
_logNetworkOnce(
|
|
_loggedNetworkReady,
|
|
'ready size=$size',
|
|
normalizedUrl,
|
|
);
|
|
return null;
|
|
case LoadState.failed:
|
|
final error = state.lastException ?? 'unknown';
|
|
_logNetworkOnce(
|
|
_loggedNetworkError,
|
|
'failed error=$error',
|
|
normalizedUrl,
|
|
);
|
|
return _buildNetworkFallback(normalizedUrl, 'image failed');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildNetworkFallback(String resource, String reason) {
|
|
_logNetworkOnce(
|
|
_loggedNetworkError,
|
|
reason,
|
|
normalizeImageResourceUrl(resource),
|
|
);
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: const Center(child: SCRotatingDotsLoading()),
|
|
);
|
|
}
|
|
|
|
void _logNetworkOnce(Set<String> bucket, String message, String url) {
|
|
if (!kDebugMode || !bucket.add('$message|$url')) {
|
|
return;
|
|
}
|
|
debugPrint('[RoomEmoji][image] $message url=$url');
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class _RestartingGifImage extends StatefulWidget {
|
|
const _RestartingGifImage({
|
|
required this.resource,
|
|
required this.width,
|
|
required this.height,
|
|
required this.fit,
|
|
required this.isLocalAsset,
|
|
});
|
|
|
|
final String resource;
|
|
final double width;
|
|
final double height;
|
|
final BoxFit fit;
|
|
final bool isLocalAsset;
|
|
|
|
@override
|
|
State<_RestartingGifImage> createState() => _RestartingGifImageState();
|
|
}
|
|
|
|
class _RestartingGifImageState extends State<_RestartingGifImage> {
|
|
static final Set<String> _loggedLoading = <String>{};
|
|
static final Set<String> _loggedReady = <String>{};
|
|
static final Set<String> _loggedError = <String>{};
|
|
|
|
Timer? _restartTimer;
|
|
int _restartToken = 0;
|
|
int _visibleToken = 0;
|
|
bool _hasCompletedOnce = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.isLocalAsset) {
|
|
_startRestartTimer();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant _RestartingGifImage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.resource != widget.resource ||
|
|
oldWidget.isLocalAsset != widget.isLocalAsset) {
|
|
_restartToken = 0;
|
|
_visibleToken = 0;
|
|
_hasCompletedOnce = false;
|
|
_restartTimer?.cancel();
|
|
if (widget.isLocalAsset) {
|
|
_startRestartTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_restartTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final resource = widget.resource.trim();
|
|
if (resource.isEmpty) {
|
|
return SizedBox(width: widget.width, height: widget.height);
|
|
}
|
|
if (widget.isLocalAsset) {
|
|
return Image.asset(
|
|
resource,
|
|
key: ValueKey('room_emoji_gif_asset_${resource}_$_restartToken'),
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
gaplessPlayback: true,
|
|
filterQuality: FilterQuality.low,
|
|
);
|
|
}
|
|
|
|
final normalizedUrl = normalizeImageResourceUrl(resource);
|
|
final foreground = _buildNetworkGifForeground(normalizedUrl);
|
|
if (!_hasCompletedOnce) {
|
|
return foreground;
|
|
}
|
|
return Stack(
|
|
children: [
|
|
_buildNetworkGifPlaceholder(normalizedUrl, _visibleToken),
|
|
foreground,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNetworkGifPlaceholder(String normalizedUrl, int token) {
|
|
return ExtendedImage.network(
|
|
normalizedUrl,
|
|
key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'),
|
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
|
cache: true,
|
|
cacheKey: '$normalizedUrl#room_emoji_gif_${token % 2}',
|
|
clearMemoryCacheWhenDispose: false,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
gaplessPlayback: true,
|
|
loadStateChanged: (state) {
|
|
switch (state.extendedImageLoadState) {
|
|
case LoadState.completed:
|
|
return null;
|
|
case LoadState.loading:
|
|
case LoadState.failed:
|
|
return SizedBox(width: widget.width, height: widget.height);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildNetworkGifForeground(String normalizedUrl) {
|
|
return ExtendedImage.network(
|
|
normalizedUrl,
|
|
key: ValueKey(
|
|
'room_emoji_gif_foreground_${normalizedUrl}_$_restartToken',
|
|
),
|
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
|
cache: true,
|
|
cacheKey: '$normalizedUrl#room_emoji_gif_${_restartToken % 2}',
|
|
clearMemoryCacheWhenDispose: true,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
fit: widget.fit,
|
|
gaplessPlayback: true,
|
|
loadStateChanged: (state) {
|
|
switch (state.extendedImageLoadState) {
|
|
case LoadState.loading:
|
|
_debugGifOnce(_loggedLoading, 'loading', normalizedUrl);
|
|
if (_hasCompletedOnce) {
|
|
return SizedBox(width: widget.width, height: widget.height);
|
|
}
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: const Center(child: SCRotatingDotsLoading()),
|
|
);
|
|
case LoadState.completed:
|
|
final image = state.extendedImageInfo?.image;
|
|
final size =
|
|
image == null ? 'unknown' : '${image.width}x${image.height}';
|
|
_debugGifOnce(_loggedReady, 'ready size=$size', normalizedUrl);
|
|
if (!_hasCompletedOnce) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_hasCompletedOnce = true;
|
|
_visibleToken = _restartToken;
|
|
});
|
|
});
|
|
} else if (_visibleToken != _restartToken) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_visibleToken = _restartToken;
|
|
});
|
|
});
|
|
}
|
|
_startRestartTimer();
|
|
return null;
|
|
case LoadState.failed:
|
|
final error = state.lastException ?? 'unknown';
|
|
_debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl);
|
|
return SizedBox(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: const Center(child: SCRotatingDotsLoading()),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
void _startRestartTimer() {
|
|
if (_restartTimer?.isActive == true) {
|
|
return;
|
|
}
|
|
_restartTimer?.cancel();
|
|
_restartTimer = Timer.periodic(_roomEmojiGifRestartInterval, (_) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_restartToken += 1;
|
|
});
|
|
});
|
|
}
|
|
|
|
static void _debugGifOnce(
|
|
Set<String> bucket,
|
|
String message,
|
|
String resource,
|
|
) {
|
|
if (!kDebugMode || !bucket.add('$message|$resource')) {
|
|
return;
|
|
}
|
|
debugPrint('[RoomEmoji][gif] $message url=$resource');
|
|
}
|
|
}
|