From de4f2397e139f659f77d47da395117c9ef37d002 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 29 Jun 2023 14:38:02 +0200 Subject: [PATCH 1/5] neon: improve loading animation of NeonCachedImage --- .../neon/lib/src/widgets/cached_image.dart | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index 43d0331a..3e1fe9b6 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -27,46 +27,34 @@ abstract class NeonCachedImage extends StatefulWidget { } class _NeonCachedImageState extends State { - late Future future = widget.getImageFile(); + Future _readImage() async { + final file = await widget.getImageFile(); + return file.readAsBytes(); + } + + late Future _future = _readImage(); @override Widget build(final BuildContext context) => Center( - child: FutureBuilder( - future: future, + child: FutureBuilder( + future: _future, builder: (final context, final fileSnapshot) { - if (fileSnapshot.hasData) { - final content = fileSnapshot.requireData.readAsBytesSync(); - - try { - // TODO: Is this safe enough? - if (widget.isSvgHint || utf8.decode(content).contains(' { color: widget.iconColor ?? Theme.of(context).colorScheme.error, ); } - return SizedBox( + + final content = fileSnapshot.requireData; + + try { + // TODO: Is this safe enough? + if (widget.isSvgHint || utf8.decode(content).contains(' Date: Thu, 29 Jun 2023 14:38:02 +0200 Subject: [PATCH 2/5] neon: unify NeonCachedImage --- packages/neon/neon/lib/neon.dart | 2 - .../lib/src/widgets/cached_api_image.dart | 32 ---------- .../neon/lib/src/widgets/cached_image.dart | 58 ++++++++++++++++++- .../lib/src/widgets/cached_url_image.dart | 15 ----- .../neon/neon/lib/src/widgets/drawer.dart | 2 +- .../neon/lib/src/widgets/user_avatar.dart | 2 +- .../neon_files/lib/widgets/file_preview.dart | 2 +- .../neon_news/lib/widgets/articles_view.dart | 2 +- .../neon/neon_news/lib/widgets/feed_icon.dart | 2 +- .../neon_notifications/lib/pages/main.dart | 2 +- 10 files changed, 62 insertions(+), 57 deletions(-) delete mode 100644 packages/neon/neon/lib/src/widgets/cached_api_image.dart delete mode 100644 packages/neon/neon/lib/src/widgets/cached_url_image.dart diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 048c8f32..ccf4aab4 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -91,9 +91,7 @@ part 'src/utils/validators.dart'; part 'src/widgets/account_settings_tile.dart'; part 'src/widgets/account_tile.dart'; part 'src/widgets/app_implementation_icon.dart'; -part 'src/widgets/cached_api_image.dart'; part 'src/widgets/cached_image.dart'; -part 'src/widgets/cached_url_image.dart'; part 'src/widgets/dialog.dart'; part 'src/widgets/exception.dart'; part 'src/widgets/image_wrapper.dart'; diff --git a/packages/neon/neon/lib/src/widgets/cached_api_image.dart b/packages/neon/neon/lib/src/widgets/cached_api_image.dart deleted file mode 100644 index 307d48dc..00000000 --- a/packages/neon/neon/lib/src/widgets/cached_api_image.dart +++ /dev/null @@ -1,32 +0,0 @@ -part of '../../neon.dart'; - -typedef APIImageDownloader = Future Function(); - -class NeonCachedApiImage extends NeonCachedImage { - NeonCachedApiImage({ - required final Account account, - required final String cacheKey, - required final APIImageDownloader download, - final String? etag, - super.size, - super.fit, - super.svgColor, - super.iconColor, - super.key, - }) : super( - getImageFile: () async { - final realKey = '${account.id}-$cacheKey'; - final cacheFile = await _cacheManager.getFileFromCache(realKey); - if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { - return cacheFile.file; - } - - return _cacheManager.putFile( - realKey, - await download(), - maxAge: const Duration(days: 7), - eTag: etag, - ); - }, - ); -} diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index 3e1fe9b6..affa4a89 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -2,8 +2,10 @@ part of '../../neon.dart'; final _cacheManager = DefaultCacheManager(); -abstract class NeonCachedImage extends StatefulWidget { - const NeonCachedImage({ +typedef APIImageDownloader = Future Function(); + +class NeonCachedImage extends StatefulWidget { + const NeonCachedImage._({ required this.getImageFile, this.isSvgHint = false, this.size, @@ -13,6 +15,58 @@ abstract class NeonCachedImage extends StatefulWidget { super.key, }); + factory NeonCachedImage.url({ + required final String url, + final Size? size, + final BoxFit? fit, + final Color? svgColor, + final Color? iconColor, + final Key? key, + }) => + NeonCachedImage._( + getImageFile: () async => _cacheManager.getSingleFile(url), + isSvgHint: Uri.parse(url).path.endsWith('.svg'), + size: size, + fit: fit, + svgColor: svgColor, + iconColor: iconColor, + key: key, + ); + + factory NeonCachedImage.api({ + required final Account account, + 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, + }) => + NeonCachedImage._( + getImageFile: () async { + final realKey = '${account.id}-$cacheKey'; + final cacheFile = await _cacheManager.getFileFromCache(realKey); + if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { + return cacheFile.file; + } + + // TODO: don't await the image being written to disk + return _cacheManager.putFile( + realKey, + await download(), + maxAge: const Duration(days: 7), + eTag: etag, + ); + }, + size: size, + fit: fit, + svgColor: svgColor, + iconColor: iconColor, + key: key, + ); + final Future Function() getImageFile; final bool isSvgHint; diff --git a/packages/neon/neon/lib/src/widgets/cached_url_image.dart b/packages/neon/neon/lib/src/widgets/cached_url_image.dart deleted file mode 100644 index 6e66e6ee..00000000 --- a/packages/neon/neon/lib/src/widgets/cached_url_image.dart +++ /dev/null @@ -1,15 +0,0 @@ -part of '../../neon.dart'; - -class NeonCachedUrlImage extends NeonCachedImage { - NeonCachedUrlImage({ - required final String url, - super.size, - super.fit, - super.svgColor, - super.iconColor, - super.key, - }) : super( - getImageFile: () => _cacheManager.getSingleFile(url), - isSvgHint: Uri.parse(url).path.endsWith('.svg'), - ); -} diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index 2cf034b4..033e036a 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -187,7 +187,7 @@ class NeonDrawerHeader extends StatelessWidget { ], if (capabilities.requireData.capabilities.theming?.logo != null) ...[ Flexible( - child: NeonCachedUrlImage( + child: NeonCachedImage.url( url: capabilities.requireData.capabilities.theming!.logo!, ), ), diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index 4909dd9d..32e23c89 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -53,7 +53,7 @@ class _UserAvatarState extends State { radius: size / 2, backgroundColor: widget.backgroundColor, child: ClipOval( - child: NeonCachedApiImage( + child: NeonCachedImage.api( account: widget.account, cacheKey: 'avatar-${widget.username}-${isDark ? 'dark' : 'light'}$pixelSize', download: () async { diff --git a/packages/neon/neon_files/lib/widgets/file_preview.dart b/packages/neon/neon_files/lib/widgets/file_preview.dart index 9de089af..657a87d9 100644 --- a/packages/neon/neon_files/lib/widgets/file_preview.dart +++ b/packages/neon/neon_files/lib/widgets/file_preview.dart @@ -34,7 +34,7 @@ class FilePreview extends StatelessWidget { builder: (final context, final showPreviewsSnapshot) { if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) { final account = Provider.of(context, listen: false).activeAccount.value!; - final child = NeonCachedApiImage( + final child = NeonCachedImage.api( account: account, cacheKey: 'preview-${details.path.join('/')}-$width-$height', etag: details.etag, diff --git a/packages/neon/neon_news/lib/widgets/articles_view.dart b/packages/neon/neon_news/lib/widgets/articles_view.dart index cc846e32..53592444 100644 --- a/packages/neon/neon_news/lib/widgets/articles_view.dart +++ b/packages/neon/neon_news/lib/widgets/articles_view.dart @@ -116,7 +116,7 @@ class _NewsArticlesViewState extends State { ), ), if (article.mediaThumbnail != null) ...[ - NeonCachedUrlImage( + NeonCachedImage.url( url: article.mediaThumbnail!, size: const Size(100, 50), fit: BoxFit.cover, diff --git a/packages/neon/neon_news/lib/widgets/feed_icon.dart b/packages/neon/neon_news/lib/widgets/feed_icon.dart index 1c9b7bb0..28dc64bd 100644 --- a/packages/neon/neon_news/lib/widgets/feed_icon.dart +++ b/packages/neon/neon_news/lib/widgets/feed_icon.dart @@ -21,7 +21,7 @@ class NewsFeedIcon extends StatelessWidget { size: Size.square(size), borderRadius: borderRadius, child: faviconLink != null && faviconLink.isNotEmpty - ? NeonCachedUrlImage( + ? NeonCachedImage.url( url: faviconLink, size: Size.square(size), iconColor: Theme.of(context).colorScheme.primary, diff --git a/packages/neon/neon_notifications/lib/pages/main.dart b/packages/neon/neon_notifications/lib/pages/main.dart index 18c76e3b..948b0d5b 100644 --- a/packages/neon/neon_notifications/lib/pages/main.dart +++ b/packages/neon/neon_notifications/lib/pages/main.dart @@ -78,7 +78,7 @@ class _NotificationsMainPageState extends State { ) : SizedBox.fromSize( size: const Size.square(40), - child: NeonCachedUrlImage( + child: NeonCachedImage.url( url: notification.icon!, size: const Size.square(40), svgColor: Theme.of(context).colorScheme.primary, From 27ce8918f92801bbb238cba910b7317aa8a341a8 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 29 Jun 2023 14:38:02 +0200 Subject: [PATCH 3/5] neon: expect Uint8List in NeonCachedImage --- .../neon/lib/src/widgets/cached_image.dart | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index affa4a89..d2c7cade 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -24,7 +24,10 @@ class NeonCachedImage extends StatefulWidget { final Key? key, }) => NeonCachedImage._( - getImageFile: () async => _cacheManager.getSingleFile(url), + getImageFile: () async { + final file = await _cacheManager.getSingleFile(url); + return file.readAsBytes(); + }, isSvgHint: Uri.parse(url).path.endsWith('.svg'), size: size, fit: fit, @@ -49,16 +52,21 @@ class NeonCachedImage extends StatefulWidget { final realKey = '${account.id}-$cacheKey'; final cacheFile = await _cacheManager.getFileFromCache(realKey); if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { - return cacheFile.file; + return cacheFile.file.readAsBytes(); } - // TODO: don't await the image being written to disk - return _cacheManager.putFile( - realKey, - await download(), - maxAge: const Duration(days: 7), - eTag: etag, + final file = await download(); + + unawaited( + _cacheManager.putFile( + realKey, + file, + maxAge: const Duration(days: 7), + eTag: etag, + ), ); + + return file; }, size: size, fit: fit, @@ -67,7 +75,7 @@ class NeonCachedImage extends StatefulWidget { key: key, ); - final Future Function() getImageFile; + final Future Function() getImageFile; final bool isSvgHint; final Size? size; @@ -81,12 +89,7 @@ class NeonCachedImage extends StatefulWidget { } class _NeonCachedImageState extends State { - Future _readImage() async { - final file = await widget.getImageFile(); - return file.readAsBytes(); - } - - late Future _future = _readImage(); + late Future _future = widget.getImageFile(); @override Widget build(final BuildContext context) => Center( @@ -108,7 +111,7 @@ class _NeonCachedImageState extends State { onRetry: () { setState(() { // ignore: discarded_futures - _future = _readImage(); + _future = widget.getImageFile(); }); }, onlyIcon: true, From 5a6891750033d44508d4b48ee2269591f7e47d10 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 29 Jun 2023 14:38:02 +0200 Subject: [PATCH 4/5] neon: require key for NeonCachedImage --- .../neon/lib/src/widgets/cached_image.dart | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index d2c7cade..ed3a7a62 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -7,12 +7,12 @@ typedef APIImageDownloader = Future Function(); class NeonCachedImage extends StatefulWidget { const NeonCachedImage._({ required this.getImageFile, + required Key super.key, this.isSvgHint = false, this.size, this.fit, this.svgColor, this.iconColor, - super.key, }); factory NeonCachedImage.url({ @@ -33,7 +33,7 @@ class NeonCachedImage extends StatefulWidget { fit: fit, svgColor: svgColor, iconColor: iconColor, - key: key, + key: key ?? Key(url), ); factory NeonCachedImage.api({ @@ -46,34 +46,35 @@ class NeonCachedImage extends StatefulWidget { final Color? svgColor, final Color? iconColor, final Key? key, - }) => - NeonCachedImage._( - getImageFile: () async { - final realKey = '${account.id}-$cacheKey'; - 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, - ); + }) { + 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 Future Function() getImageFile; final bool isSvgHint; From 4f0009939e820b825d6e0f72d490feadc776571d Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 30 Jun 2023 09:36:16 +0200 Subject: [PATCH 5/5] 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, + ); +}