diff --git a/packages/neon/lib/src/apps/files/app.dart b/packages/neon/lib/src/apps/files/app.dart index 480c3eb1..2915de6f 100644 --- a/packages/neon/lib/src/apps/files/app.dart +++ b/packages/neon/lib/src/apps/files/app.dart @@ -19,7 +19,6 @@ import 'package:neon/src/apps/files/blocs/browser.dart'; import 'package:neon/src/apps/files/blocs/files.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/apps.dart'; -import 'package:neon/src/models/account.dart'; import 'package:neon/src/neon.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:path/path.dart' as p; diff --git a/packages/neon/lib/src/apps/files/widgets/file_preview.dart b/packages/neon/lib/src/apps/files/widgets/file_preview.dart index 16822ab0..00e0906f 100644 --- a/packages/neon/lib/src/apps/files/widgets/file_preview.dart +++ b/packages/neon/lib/src/apps/files/widgets/file_preview.dart @@ -32,81 +32,26 @@ class FilePreview extends StatelessWidget { child: StreamBuilder( stream: bloc.options.showPreviewsOption.stream, builder: (final context, final showPreviewsSnapshot) { - if (!showPreviewsSnapshot.hasData) { - return Container(); - } - if (showPreviewsSnapshot.data! && (details.hasPreview ?? false) && details.etag != null) { - return Builder( - builder: (final context) { - final account = RxBlocProvider.of(context).activeAccount.value; - if (account == null) { - return Container(); - } - - final stream = Provider.of(context).wrapBytes( - account.client.id, - 'files-preview-${details.etag}-$width-$height', - () async => account.client.core.getPreview( - file: details.path.join('/'), - x: width, - y: height, - ), - preferCache: true, - ); - - return ResultStreamBuilder( - stream: stream, - builder: ( - final context, - final previewData, - final previewError, - final previewLoading, - ) => - Stack( - children: [ - if (previewData != null) ...[ - Center( - child: Builder( - builder: (final context) { - final child = Image.memory( - previewData, - fit: BoxFit.cover, - width: width.toDouble(), - height: height.toDouble(), - ); - if (withBackground) { - return ImageWrapper( - color: Colors.white, - borderRadius: borderRadius, - child: child, - ); - } - return child; - }, - ), - ), - ], - if (previewError != null) ...[ - Center( - child: Icon( - Icons.error_outline, - size: min(width.toDouble(), height.toDouble()), - color: color, - ), - ), - ], - if (previewLoading) ...[ - Center( - child: CustomLinearProgressIndicator( - color: color, - ), - ), - ], - ], - ), - ); - }, + if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) { + final account = RxBlocProvider.of(context).activeAccount.value!; + final child = CachedAPIImage( + 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 ImageWrapper( + color: Colors.white, + borderRadius: borderRadius, + child: child, + ); + } + return child; } if (details.isDirectory) { diff --git a/packages/neon/lib/src/neon.dart b/packages/neon/lib/src/neon.dart index 628ec94c..c2a0bb66 100644 --- a/packages/neon/lib/src/neon.dart +++ b/packages/neon/lib/src/neon.dart @@ -88,6 +88,8 @@ part 'utils/theme.dart'; part 'utils/validators.dart'; part 'widgets/account_avatar.dart'; part 'widgets/account_tile.dart'; +part 'widgets/cached_api_image.dart'; +part 'widgets/cached_image.dart'; part 'widgets/cached_url_image.dart'; part 'widgets/custom_dialog.dart'; part 'widgets/custom_linear_progress_indicator.dart'; diff --git a/packages/neon/lib/src/widgets/account_avatar.dart b/packages/neon/lib/src/widgets/account_avatar.dart index 38a2c4c4..5afa3d52 100644 --- a/packages/neon/lib/src/widgets/account_avatar.dart +++ b/packages/neon/lib/src/widgets/account_avatar.dart @@ -11,96 +11,73 @@ class AccountAvatar extends StatelessWidget { final Account account; @override - Widget build(final BuildContext context) => Stack( - alignment: Alignment.center, - children: [ - ResultStreamBuilder( - // TODO: See TODO in cached_url_image.dart - stream: Provider.of(context, listen: false).wrapBytes( - account.client.id, - 'accounts-avatar-${account.id}', - () async => account.client.core.getAvatar( + Widget build(final BuildContext context) { + final size = (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt(); + return Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: kAvatarSize / 2, + child: ClipOval( + child: CachedAPIImage( + account: account, + cacheKey: 'avatar-${account.id}-$size', + download: () async => account.client.core.getAvatar( userId: account.username, - size: (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt(), + size: size, ), - preferCache: true, - ), - builder: ( - final context, - final avatarData, - final avatarError, - final avatarLoading, - ) => - Stack( - children: [ - if (avatarData != null) ...[ - CircleAvatar( - radius: kAvatarSize / 2, - backgroundImage: MemoryImage(avatarData), - ), - ], - if (avatarError != null) ...[ - Icon( - Icons.error_outline, - size: 30, - color: Theme.of(context).colorScheme.onBackground, - ), - ], - if (avatarLoading) ...[ - const CustomLinearProgressIndicator(), - ], - ], ), ), - StandardRxResultBuilder( - bloc: RxBlocProvider.of(context).getUserStatusBloc(account), - state: (final bloc) => bloc.userStatus, - builder: ( - final context, - final userStatusData, - final userStatusError, - final userStatusLoading, - final _, - ) => - SizedBox( - height: kAvatarSize, - width: kAvatarSize, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - height: kAvatarSize / 3, - width: kAvatarSize / 3, - decoration: userStatusLoading || userStatusError != null || userStatusData == null - ? null - : BoxDecoration( - shape: BoxShape.circle, - color: _userStatusToColor(userStatusData), - border: userStatusData.status != UserStatusType.offline && - userStatusData.status != UserStatusType.invisible - ? Border.all( - color: Theme.of(context).colorScheme.onPrimary, - ) - : null, - ), - child: userStatusLoading - ? CircularProgressIndicator( - strokeWidth: 1.5, - color: Theme.of(context).colorScheme.onPrimary, - ) - : userStatusError != null && - (userStatusError is! ApiException || userStatusError.statusCode != 404) - ? const Icon( - Icons.error_outline, - size: kAvatarSize / 3, - color: Colors.red, - ) - : null, - ), + ), + StandardRxResultBuilder( + bloc: RxBlocProvider.of(context).getUserStatusBloc(account), + state: (final bloc) => bloc.userStatus, + builder: ( + final context, + final userStatusData, + final userStatusError, + final userStatusLoading, + final _, + ) => + SizedBox( + height: kAvatarSize, + width: kAvatarSize, + child: Align( + alignment: Alignment.bottomRight, + child: Container( + height: kAvatarSize / 3, + width: kAvatarSize / 3, + decoration: userStatusLoading || userStatusError != null || userStatusData == null + ? null + : BoxDecoration( + shape: BoxShape.circle, + color: _userStatusToColor(userStatusData), + border: userStatusData.status != UserStatusType.offline && + userStatusData.status != UserStatusType.invisible + ? Border.all( + color: Theme.of(context).colorScheme.onPrimary, + ) + : null, + ), + child: userStatusLoading + ? CircularProgressIndicator( + strokeWidth: 1.5, + color: Theme.of(context).colorScheme.onPrimary, + ) + : userStatusError != null && (userStatusError is! ApiException || userStatusError.statusCode != 404) + ? const Icon( + Icons.error_outline, + size: kAvatarSize / 3, + color: Colors.red, + ) + : null, ), ), ), - ], - ); + ), + ], + ); + } Color _userStatusToColor(final UserStatus userStatus) { switch (userStatus.status) { diff --git a/packages/neon/lib/src/widgets/cached_api_image.dart b/packages/neon/lib/src/widgets/cached_api_image.dart new file mode 100644 index 00000000..0350e537 --- /dev/null +++ b/packages/neon/lib/src/widgets/cached_api_image.dart @@ -0,0 +1,33 @@ +part of '../neon.dart'; + +typedef APIImageDownloader = Future Function(); + +class CachedAPIImage extends CachedImage { + CachedAPIImage({ + required final Account account, + required final String cacheKey, + required final APIImageDownloader download, + final String? etag, + super.height, + super.width, + super.fit, + super.svgColor, + super.iconColor, + super.key, + }) : super( + future: () 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/lib/src/widgets/cached_image.dart b/packages/neon/lib/src/widgets/cached_image.dart new file mode 100644 index 00000000..fb9aa07f --- /dev/null +++ b/packages/neon/lib/src/widgets/cached_image.dart @@ -0,0 +1,72 @@ +part of '../neon.dart'; + +final _cacheManager = DefaultCacheManager(); + +abstract class CachedImage extends StatelessWidget { + const CachedImage({ + required this.future, + this.isSvgHint = false, + this.height, + this.width, + this.fit, + this.svgColor, + this.iconColor, + super.key, + }); + + final Future future; + final bool isSvgHint; + + final double? height; + final double? width; + final BoxFit? fit; + + final Color? svgColor; + final Color? iconColor; + + @override + Widget build(final BuildContext context) => FutureBuilder( + future: future, + builder: (final context, final fileSnapshot) { + if (fileSnapshot.hasData) { + final content = fileSnapshot.data!.readAsBytesSync(); + + try { + // TODO: Is this safe enough? + if (isSvgHint || utf8.decode(content).contains(' FutureBuilder( - // Really weird false positive - // ignore: discarded_futures - future: _cacheManager.getSingleFile(url), - builder: (final context, final fileSnapshot) { - if (fileSnapshot.hasData) { - final content = fileSnapshot.data!.readAsBytesSync(); - - var isSvg = false; - if (Uri.parse(url).path.endsWith('.svg')) { - isSvg = true; - } - if (!isSvg) { - try { - // TODO: Is this safe enough? - if (utf8.decode(content).contains('