mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
better scrolling
This commit is contained in:
parent
c988342de1
commit
3100702e93
8 changed files with 233 additions and 88 deletions
|
|
@ -32,3 +32,31 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics {
|
|||
damping: 80,
|
||||
);
|
||||
}
|
||||
|
||||
class ScrollUnawareScrollPhysics extends ScrollPhysics {
|
||||
const ScrollUnawareScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
ScrollUnawareScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return ScrollUnawareScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollUnawareClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const ScrollUnawareClampingScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
ScrollUnawareClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return ScrollUnawareClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ abstract class ImageRequest {
|
|||
return;
|
||||
}
|
||||
_isCancelled = true;
|
||||
if (!kReleaseMode) {
|
||||
debugPrint('Cancelling image request $requestId');
|
||||
}
|
||||
return _onCancelled();
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +40,9 @@ abstract class ImageRequest {
|
|||
Future<ui.FrameInfo?> _fromPlatformImage(Map<String, int> info) async {
|
||||
final address = info['pointer'];
|
||||
if (address == null) {
|
||||
if (!kReleaseMode) {
|
||||
debugPrint('Platform image request for $requestId was cancelled');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +90,15 @@ class ThumbhashImageRequest extends ImageRequest {
|
|||
return null;
|
||||
}
|
||||
|
||||
Stopwatch? stopwatch;
|
||||
if (!kReleaseMode) {
|
||||
stopwatch = Stopwatch()..start();
|
||||
}
|
||||
final Map<String, int> info = await thumbnailApi.getThumbhash(thumbhash);
|
||||
if (!kReleaseMode) {
|
||||
stopwatch!.stop();
|
||||
debugPrint('Thumbhash request $requestId took ${stopwatch.elapsedMilliseconds} ms');
|
||||
}
|
||||
final frame = await _fromPlatformImage(info);
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
|
@ -108,12 +122,20 @@ class LocalImageRequest extends ImageRequest {
|
|||
return null;
|
||||
}
|
||||
|
||||
Stopwatch? stopwatch;
|
||||
if (!kReleaseMode) {
|
||||
stopwatch = Stopwatch()..start();
|
||||
}
|
||||
final Map<String, int> info = await thumbnailApi.requestImage(
|
||||
localId,
|
||||
requestId: requestId,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
if (!kReleaseMode) {
|
||||
stopwatch!.stop();
|
||||
debugPrint('Local image request $requestId took ${stopwatch.elapsedMilliseconds} ms');
|
||||
}
|
||||
final frame = await _fromPlatformImage(info);
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
|
@ -127,7 +149,7 @@ class LocalImageRequest extends ImageRequest {
|
|||
class RemoteImageRequest extends ImageRequest {
|
||||
static final log = Logger('RemoteImageRequest');
|
||||
static final cacheManager = RemoteImageCacheManager();
|
||||
static final client = HttpClient();
|
||||
static final client = HttpClient()..maxConnectionsPerHost = 32;
|
||||
String uri;
|
||||
Map<String, String> headers;
|
||||
HttpClientRequest? _request;
|
||||
|
|
@ -140,29 +162,42 @@ class RemoteImageRequest extends ImageRequest {
|
|||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// The DB calls made by the cache manager are a *massive* bottleneck (6+ seconds) with high concurrency.
|
||||
// Since it isn't possible to cancel these operations, we only prefer the cache when they can be avoided.
|
||||
// The DB hit is left as a fallback for offline use.
|
||||
final cachedFileBuffer = await _loadCachedFile(uri, inMemoryOnly: true);
|
||||
if (cachedFileBuffer != null) {
|
||||
return _decodeBuffer(cachedFileBuffer, decode, scale);
|
||||
// TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled,
|
||||
// so it just makes things slower and more memory hungry. Even just saving files to disk
|
||||
// for offline use adds too much overhead as these calls add up. We only prefer fetching from it when
|
||||
// it can skip the DB call.
|
||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true);
|
||||
if (cachedFileImage != null) {
|
||||
return cachedFileImage;
|
||||
}
|
||||
|
||||
try {
|
||||
Stopwatch? stopwatch;
|
||||
if (!kReleaseMode) {
|
||||
stopwatch = Stopwatch()..start();
|
||||
}
|
||||
final buffer = await _downloadImage(uri);
|
||||
if (buffer == null || _isCancelled) {
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
if (!kReleaseMode) {
|
||||
stopwatch!.stop();
|
||||
debugPrint('Remote image download request $requestId took ${stopwatch.elapsedMilliseconds} ms');
|
||||
}
|
||||
return await _decodeBuffer(buffer, decode, scale);
|
||||
} catch (e) {
|
||||
if (e is HttpException && (e.message.endsWith('aborted') || e.message.startsWith('Connection closed'))) {
|
||||
if (_isCancelled) {
|
||||
if (!kReleaseMode) {
|
||||
debugPrint('Remote image download request for $requestId was cancelled');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
log.severe('Failed to load remote image', e);
|
||||
final buffer = await _loadCachedFile(uri, inMemoryOnly: false);
|
||||
if (buffer != null) {
|
||||
return _decodeBuffer(buffer, decode, scale);
|
||||
|
||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false);
|
||||
if (cachedFileImage != null) {
|
||||
return cachedFileImage;
|
||||
}
|
||||
|
||||
rethrow;
|
||||
} finally {
|
||||
_request = null;
|
||||
|
|
@ -170,45 +205,59 @@ class RemoteImageRequest extends ImageRequest {
|
|||
}
|
||||
|
||||
Future<ImmutableBuffer?> _downloadImage(String url) async {
|
||||
final request = _request = await client.getUrl(Uri.parse(url));
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final request = _request = await client.getUrl(Uri.parse(url));
|
||||
if (_isCancelled) {
|
||||
request.abort();
|
||||
return _request = null;
|
||||
}
|
||||
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
for (final entry in headers.entries) {
|
||||
request.headers.set(entry.key, entry.value);
|
||||
}
|
||||
final response = await request.close();
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final bytes = await consolidateHttpClientResponseBytes(response);
|
||||
_cacheFile(url, bytes);
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
return await ImmutableBuffer.fromUint8List(bytes);
|
||||
}
|
||||
|
||||
Future<void> _cacheFile(String url, Uint8List bytes) async {
|
||||
try {
|
||||
await cacheManager.putFile(url, bytes);
|
||||
} catch (e) {
|
||||
log.severe('Failed to cache image', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ImmutableBuffer?> _loadCachedFile(String url, {required bool inMemoryOnly}) async {
|
||||
Future<ImageInfo?> _loadCachedFile(
|
||||
String url,
|
||||
ImageDecoderCallback decode,
|
||||
double scale, {
|
||||
required bool inMemoryOnly,
|
||||
}) async {
|
||||
if (_isCancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url));
|
||||
if (_isCancelled || file == null) {
|
||||
return null;
|
||||
}
|
||||
return await ImmutableBuffer.fromFilePath(file.file.path);
|
||||
|
||||
try {
|
||||
final buffer = await ImmutableBuffer.fromFilePath(file.file.path);
|
||||
return await _decodeBuffer(buffer, decode, scale);
|
||||
} catch (e) {
|
||||
log.severe('Failed to decode cached image', e);
|
||||
_evictFile(url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _evictFile(String url) async {
|
||||
try {
|
||||
await cacheManager.removeFile(url);
|
||||
} catch (e) {
|
||||
log.severe('Failed to remove cached image', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
class Thumbnail extends StatelessWidget {
|
||||
const Thumbnail({this.asset, this.remoteId, this.size = const Size.square(256), this.fit = BoxFit.cover, super.key})
|
||||
class Thumbnail extends StatefulWidget {
|
||||
const Thumbnail({this.asset, this.remoteId, this.size = kTimelineFixedTileExtent, this.fit = BoxFit.cover, super.key})
|
||||
: assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
||||
|
||||
final BaseAsset? asset;
|
||||
|
|
@ -16,46 +17,79 @@ class Thumbnail extends StatelessWidget {
|
|||
final Size size;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
createState() => _ThumbnailState();
|
||||
}
|
||||
|
||||
class _ThumbnailState extends State<Thumbnail> {
|
||||
ImageProvider? provider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
provider = getThumbnailImageProvider(asset: widget.asset, remoteId: widget.remoteId);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant Thumbnail oldWidget) {
|
||||
if (oldWidget.asset == widget.asset && oldWidget.remoteId == widget.remoteId) {
|
||||
return;
|
||||
}
|
||||
if (provider is CancellableImageProvider) {
|
||||
(provider as CancellableImageProvider).cancel();
|
||||
}
|
||||
provider = getThumbnailImageProvider(asset: widget.asset, remoteId: widget.remoteId);
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId);
|
||||
final thumbHash = widget.asset is RemoteAsset ? (widget.asset as RemoteAsset).thumbHash : null;
|
||||
|
||||
return OctoImage.fromSet(
|
||||
image: provider,
|
||||
image: provider!,
|
||||
octoSet: OctoSet(
|
||||
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash, fit: fit),
|
||||
errorBuilder: _blurHashErrorBuilder(thumbHash, provider: provider, fit: fit, asset: asset),
|
||||
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash),
|
||||
errorBuilder: _blurHashErrorBuilder(thumbHash, provider: provider, asset: widget.asset),
|
||||
),
|
||||
fadeOutDuration: const Duration(milliseconds: 100),
|
||||
fadeInDuration: Duration.zero,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
fit: fit,
|
||||
width: widget.size.width,
|
||||
height: widget.size.height,
|
||||
fit: widget.fit,
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
|
||||
@override
|
||||
void dispose() {
|
||||
if (provider is CancellableImageProvider) {
|
||||
(provider as CancellableImageProvider).cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash) {
|
||||
return (context) => thumbHash == null
|
||||
? const ThumbnailPlaceholder()
|
||||
: FadeInPlaceholderImage(
|
||||
placeholder: const ThumbnailPlaceholder(),
|
||||
image: ThumbHashProvider(thumbHash: thumbHash),
|
||||
fit: fit ?? BoxFit.cover,
|
||||
fit: widget.fit,
|
||||
width: widget.size.width,
|
||||
height: widget.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) =>
|
||||
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider}) =>
|
||||
(context, e, s) {
|
||||
Logger("ImThumbnail").warning("Error loading thumbnail for ${asset?.name}", e, s);
|
||||
provider?.evict();
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_blurHashPlaceholderBuilder(blurhash, fit: fit)(context),
|
||||
_blurHashPlaceholderBuilder(blurhash)(context),
|
||||
const Opacity(opacity: 0.75, child: Icon(Icons.error_outline_rounded)),
|
||||
],
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
|
|
@ -106,7 +107,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||
bool _dragging = false;
|
||||
TimelineAssetIndex? _dragAnchorIndex;
|
||||
final Set<BaseAsset> _draggedAssets = HashSet();
|
||||
ScrollPhysics? _scrollPhysics;
|
||||
ScrollPhysics _scrollPhysics = const ScrollUnawareScrollPhysics();
|
||||
|
||||
int _perRow = 4;
|
||||
double _scaleFactor = 3.0;
|
||||
|
|
@ -188,7 +189,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||
// Drag selection methods
|
||||
void _setDragStartIndex(TimelineAssetIndex index) {
|
||||
setState(() {
|
||||
_scrollPhysics = const ClampingScrollPhysics();
|
||||
_scrollPhysics = const ScrollUnawareClampingScrollPhysics();
|
||||
_dragAnchorIndex = index;
|
||||
_dragging = true;
|
||||
});
|
||||
|
|
@ -198,7 +199,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Update the physics post frame to prevent sudden change in physics on iOS.
|
||||
setState(() {
|
||||
_scrollPhysics = null;
|
||||
_scrollPhysics = const ScrollUnawareScrollPhysics();
|
||||
});
|
||||
});
|
||||
setState(() {
|
||||
|
|
|
|||
19
mobile/lib/utils/cache/custom_image_cache.dart
vendored
19
mobile/lib/utils/cache/custom_image_cache.dart
vendored
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||
|
|
@ -9,6 +10,7 @@ import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.d
|
|||
/// [ImageCache] that uses two caches for small and large images
|
||||
/// so that a single large image does not evict all small images
|
||||
final class CustomImageCache implements ImageCache {
|
||||
final _thumbhash = ImageCache()..maximumSize = 0;
|
||||
final _small = ImageCache();
|
||||
final _large = ImageCache()..maximumSize = 5; // Maximum 5 images
|
||||
|
||||
|
|
@ -39,13 +41,16 @@ final class CustomImageCache implements ImageCache {
|
|||
/// Gets the cache for the given key
|
||||
/// [_large] is used for [ImmichLocalImageProvider] and [ImmichRemoteImageProvider]
|
||||
/// [_small] is used for [ImmichLocalThumbnailProvider] and [ImmichRemoteThumbnailProvider]
|
||||
ImageCache _cacheForKey(Object key) =>
|
||||
(key is ImmichLocalImageProvider ||
|
||||
key is ImmichRemoteImageProvider ||
|
||||
key is LocalFullImageProvider ||
|
||||
key is RemoteFullImageProvider)
|
||||
? _large
|
||||
: _small;
|
||||
ImageCache _cacheForKey(Object key) {
|
||||
return switch (key) {
|
||||
ImmichLocalImageProvider() ||
|
||||
ImmichRemoteImageProvider() ||
|
||||
LocalFullImageProvider() ||
|
||||
RemoteFullImageProvider() => _large,
|
||||
ThumbHashProvider() => _thumbhash,
|
||||
_ => _small,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool containsKey(Object key) {
|
||||
|
|
|
|||
|
|
@ -1,30 +1,43 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
||||
|
||||
class FadeInPlaceholderImage extends StatelessWidget {
|
||||
final Widget placeholder;
|
||||
final ImageProvider image;
|
||||
final Duration duration;
|
||||
final BoxFit fit;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const FadeInPlaceholderImage({
|
||||
super.key,
|
||||
required this.placeholder,
|
||||
required this.image,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.duration = const Duration(milliseconds: 100),
|
||||
this.fit = BoxFit.cover,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
placeholder,
|
||||
FadeInImage(fadeInDuration: duration, image: image, fit: fit, placeholder: MemoryImage(kTransparentImage)),
|
||||
],
|
||||
),
|
||||
final stopwatch = Stopwatch()..start();
|
||||
return Image(
|
||||
image: image,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (frame == null) {
|
||||
return AnimatedSwitcher(duration: duration, child: placeholder);
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
if (stopwatch.elapsedMilliseconds < 32) {
|
||||
return child;
|
||||
}
|
||||
return AnimatedSwitcher(duration: duration, child: child);
|
||||
},
|
||||
filterQuality: FilterQuality.low,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ImmichThumbnail extends HookConsumerWidget {
|
|||
customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) {
|
||||
thumbnailProviderInstance.evict();
|
||||
|
||||
final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit);
|
||||
final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, width: width, height: height, fit: fit);
|
||||
return originalErrorWidgetBuilder(ctx, error, stackTrace);
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ class ImmichThumbnail extends HookConsumerWidget {
|
|||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: const Duration(milliseconds: 100),
|
||||
octoSet: OctoSet(
|
||||
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, width: width, height: height, fit: fit),
|
||||
errorBuilder: customErrorBuilder,
|
||||
),
|
||||
image: thumbnailProviderInstance,
|
||||
|
|
|
|||
|
|
@ -6,25 +6,40 @@ import 'package:octo_image/octo_image.dart';
|
|||
|
||||
/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
|
||||
/// placeholder and [OctoError.icon] as error.
|
||||
OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) {
|
||||
OctoSet blurHashOrPlaceholder(
|
||||
Uint8List? blurhash, {
|
||||
required double width,
|
||||
required double height,
|
||||
BoxFit? fit,
|
||||
Text? errorMessage,
|
||||
}) {
|
||||
return OctoSet(
|
||||
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||
errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage),
|
||||
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, width: width, height: height, fit: fit),
|
||||
errorBuilder: blurHashErrorBuilder(blurhash, width: width, height: height, fit: fit, message: errorMessage),
|
||||
);
|
||||
}
|
||||
|
||||
OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) {
|
||||
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
||||
Uint8List? blurhash, {
|
||||
required double width,
|
||||
required double height,
|
||||
BoxFit? fit,
|
||||
}) {
|
||||
return (context) => blurhash == null
|
||||
? const ThumbnailPlaceholder()
|
||||
: FadeInPlaceholderImage(
|
||||
placeholder: const ThumbnailPlaceholder(),
|
||||
image: MemoryImage(blurhash),
|
||||
fit: fit ?? BoxFit.cover,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
OctoErrorBuilder blurHashErrorBuilder(
|
||||
Uint8List? blurhash, {
|
||||
required double width,
|
||||
required double height,
|
||||
BoxFit? fit,
|
||||
Text? message,
|
||||
IconData? icon,
|
||||
|
|
@ -32,7 +47,7 @@ OctoErrorBuilder blurHashErrorBuilder(
|
|||
double? iconSize,
|
||||
}) {
|
||||
return OctoError.placeholderWithErrorIcon(
|
||||
blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||
blurHashPlaceholderBuilder(blurhash, width: width, height: width, fit: fit),
|
||||
message: message,
|
||||
icon: icon,
|
||||
iconColor: iconColor,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue