From 4f0009939e820b825d6e0f72d490feadc776571d Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 30 Jun 2023 09:36:16 +0200 Subject: [PATCH] neon, neon_files: make NeonCachedImage easily extendable The new FilePreviewImage now also uses the mimetype as as svgHint --- .../neon/lib/src/widgets/cached_image.dart | 146 +++++++++--------- .../neon/lib/src/widgets/user_avatar.dart | 11 +- .../neon_files/lib/widgets/file_preview.dart | 109 +++++++++---- 3 files changed, 159 insertions(+), 107 deletions(-) diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index ed3a7a62..1a1ac7f6 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -1,12 +1,12 @@ part of '../../neon.dart'; -final _cacheManager = DefaultCacheManager(); - -typedef APIImageDownloader = Future Function(); +typedef CacheReviver = FutureOr Function(CacheManager cacheManager); +typedef ImageDownloader = FutureOr Function(); +typedef CacheWriter = Future Function(CacheManager cacheManager, Uint8List image); class NeonCachedImage extends StatefulWidget { - const NeonCachedImage._({ - required this.getImageFile, + const NeonCachedImage({ + required this.image, required Key super.key, this.isSvgHint = false, this.size, @@ -15,68 +15,36 @@ class NeonCachedImage extends StatefulWidget { this.iconColor, }); - factory NeonCachedImage.url({ + NeonCachedImage.url({ required final String url, - final Size? size, - final BoxFit? fit, - final Color? svgColor, - final Color? iconColor, final Key? key, - }) => - NeonCachedImage._( - getImageFile: () async { - final file = await _cacheManager.getSingleFile(url); - return file.readAsBytes(); - }, - isSvgHint: Uri.parse(url).path.endsWith('.svg'), - size: size, - fit: fit, - svgColor: svgColor, - iconColor: iconColor, - key: key ?? Key(url), - ); + this.isSvgHint = false, + this.size, + this.fit, + this.svgColor, + this.iconColor, + }) : image = _getImageFromUrl(url), + super(key: key ?? Key(url)); - factory NeonCachedImage.api({ - required final Account account, + NeonCachedImage.custom({ + required final ImageDownloader getImage, required final String cacheKey, - required final APIImageDownloader download, - final String? etag, - final Size? size, - final BoxFit? fit, - final Color? svgColor, - final Color? iconColor, - final Key? key, - }) { - final realKey = '${account.id}-$cacheKey'; - return NeonCachedImage._( - getImageFile: () async { - final cacheFile = await _cacheManager.getFileFromCache(realKey); - if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { - return cacheFile.file.readAsBytes(); - } - - final file = await download(); - - unawaited( - _cacheManager.putFile( - realKey, - file, - maxAge: const Duration(days: 7), - eTag: etag, - ), - ); - - return file; - }, - size: size, - fit: fit, - svgColor: svgColor, - iconColor: iconColor, - key: key ?? Key(realKey), - ); - } + final CacheReviver? reviver, + final CacheWriter? writeCache, + this.isSvgHint = false, + this.size, + this.fit, + this.svgColor, + this.iconColor, + }) : image = _customImageGetter( + reviver, + getImage, + writeCache, + cacheKey, + ), + super(key: Key(cacheKey)); - final Future Function() getImageFile; + final Future image; final bool isSvgHint; final Size? size; @@ -85,17 +53,60 @@ class NeonCachedImage extends StatefulWidget { final Color? svgColor; final Color? iconColor; + static Future _getImageFromUrl(final String url) async { + final file = await _cacheManager.getSingleFile(url); + return file.readAsBytes(); + } + + static Future _customImageGetter( + final CacheReviver? checkCache, + final ImageDownloader getImage, + final CacheWriter? writeCache, + final String cacheKey, + ) async { + final cached = await checkCache?.call(_cacheManager) ?? await _defaultCacheReviver(cacheKey); + if (cached != null) { + return cached; + } + + final data = await getImage(); + + unawaited(writeCache?.call(_cacheManager, data) ?? _defaultCacheWriter(data, cacheKey)); + + return data; + } + + static Future _defaultCacheReviver(final String cacheKey) async { + final cacheFile = await _cacheManager.getFileFromCache(cacheKey); + if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { + return cacheFile.file.readAsBytes(); + } + + return null; + } + + static Future _defaultCacheWriter( + final Uint8List data, + final String cacheKey, + ) async { + await _cacheManager.putFile( + cacheKey, + data, + maxAge: const Duration(days: 7), + ); + } + + static final _cacheManager = DefaultCacheManager(); + @override State createState() => _NeonCachedImageState(); } class _NeonCachedImageState extends State { - late Future _future = widget.getImageFile(); - @override Widget build(final BuildContext context) => Center( child: FutureBuilder( - future: _future, + future: widget.image, builder: (final context, final fileSnapshot) { if (!fileSnapshot.hasData) { return SizedBox( @@ -110,10 +121,7 @@ class _NeonCachedImageState extends State { return NeonException( fileSnapshot.error, onRetry: () { - setState(() { - // ignore: discarded_futures - _future = widget.getImageFile(); - }); + setState(() {}); }, onlyIcon: true, iconSize: widget.size?.shortestSide, diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index 32e23c89..51d38c41 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -43,7 +43,7 @@ class _UserAvatarState extends State { @override Widget build(final BuildContext context) => LayoutBuilder( builder: (final context, final constraints) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final brightness = Theme.of(context).brightness; size = constraints.constrain(Size.square(widget.size)).shortestSide; final pixelSize = (size * MediaQuery.of(context).devicePixelRatio).toInt(); return Stack( @@ -53,11 +53,10 @@ class _UserAvatarState extends State { radius: size / 2, backgroundColor: widget.backgroundColor, child: ClipOval( - child: NeonCachedImage.api( - account: widget.account, - cacheKey: 'avatar-${widget.username}-${isDark ? 'dark' : 'light'}$pixelSize', - download: () async { - if (isDark) { + child: NeonCachedImage.custom( + cacheKey: '${widget.account.id}-avatar-${widget.username}-$brightness$pixelSize', + getImage: () async { + if (brightness == Brightness.dark) { return widget.account.client.core.getDarkAvatar( userId: widget.username, size: pixelSize, diff --git a/packages/neon/neon_files/lib/widgets/file_preview.dart b/packages/neon/neon_files/lib/widgets/file_preview.dart index 657a87d9..7a610e78 100644 --- a/packages/neon/neon_files/lib/widgets/file_preview.dart +++ b/packages/neon/neon_files/lib/widgets/file_preview.dart @@ -21,39 +21,14 @@ class FilePreview extends StatelessWidget { final BorderRadius? borderRadius; final bool withBackground; - int get width => size.width.toInt(); - int get height => size.height.toInt(); - @override Widget build(final BuildContext context) { final color = this.color ?? Theme.of(context).colorScheme.primary; + return SizedBox.fromSize( size: size, - child: StreamBuilder( - stream: bloc.options.showPreviewsOption.stream, - builder: (final context, final showPreviewsSnapshot) { - if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) { - final account = Provider.of(context, listen: false).activeAccount.value!; - final child = NeonCachedImage.api( - account: account, - cacheKey: 'preview-${details.path.join('/')}-$width-$height', - etag: details.etag, - download: () async => account.client.core.getPreview( - file: details.path.join('/'), - x: width, - y: height, - ), - ); - if (withBackground) { - return NeonImageWrapper( - color: Colors.white, - borderRadius: borderRadius, - child: child, - ); - } - return child; - } - + child: Builder( + builder: (final context) { if (details.isDirectory) { return Icon( MdiIcons.folder, @@ -62,13 +37,83 @@ class FilePreview extends StatelessWidget { ); } - return FileIcon( - details.name, - color: color, - size: size.shortestSide, + return OptionBuilder( + option: bloc.options.showPreviewsOption, + builder: (final context, final showPreviewsSnapshot) { + if (showPreviewsSnapshot && (details.hasPreview ?? false)) { + final account = Provider.of(context, listen: false).activeAccount.value!; + final child = FilePreviewImage( + account: account, + file: details, + size: size, + ); + if (withBackground) { + return NeonImageWrapper( + color: Colors.white, + borderRadius: borderRadius, + child: child, + ); + } + return child; + } + + return FileIcon( + details.name, + color: color, + size: size.shortestSide, + ); + }, ); }, ), ); } } + +class FilePreviewImage extends NeonCachedImage { + factory FilePreviewImage({ + required final Account account, + required final FileDetails file, + required final Size size, + }) { + final width = size.width.toInt(); + final height = size.height.toInt(); + final path = file.path.join('/'); + final cacheKey = '${account.id}-preview-$path-$width-$height'; + + return FilePreviewImage._( + account: account, + file: file, + size: size, + cacheKey: cacheKey, + path: path, + width: width, + height: height, + ); + } + + FilePreviewImage._({ + required final Account account, + required final FileDetails file, + required Size super.size, + required super.cacheKey, + required final String path, + required final int width, + required final int height, + }) : super.custom( + getImage: () async => account.client.core.getPreview( + file: path, + x: width, + y: height, + ), + writeCache: (final cacheManager, final data) async { + await cacheManager.putFile( + cacheKey, + data, + maxAge: const Duration(days: 7), + eTag: file.etag, + ); + }, + isSvgHint: file.mimeType?.contains('svg') ?? false, + ); +}