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 43d0331a..1a1ac7f6 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -1,19 +1,50 @@ part of '../../neon.dart'; -final _cacheManager = DefaultCacheManager(); +typedef CacheReviver = FutureOr Function(CacheManager cacheManager); +typedef ImageDownloader = FutureOr Function(); +typedef CacheWriter = Future Function(CacheManager cacheManager, Uint8List image); -abstract class NeonCachedImage extends StatefulWidget { +class NeonCachedImage extends StatefulWidget { const NeonCachedImage({ - required this.getImageFile, + required this.image, + required Key super.key, this.isSvgHint = false, this.size, this.fit, this.svgColor, this.iconColor, - super.key, }); - final Future Function() getImageFile; + NeonCachedImage.url({ + required final String url, + final Key? key, + this.isSvgHint = false, + this.size, + this.fit, + this.svgColor, + this.iconColor, + }) : image = _getImageFromUrl(url), + super(key: key ?? Key(url)); + + NeonCachedImage.custom({ + required final ImageDownloader getImage, + required final String cacheKey, + 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 image; final bool isSvgHint; final Size? size; @@ -22,63 +53,105 @@ abstract 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, + child: FutureBuilder( + future: widget.image, 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(' _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..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: NeonCachedApiImage( - 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 9de089af..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 = NeonCachedApiImage( - 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, + ); +} 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,