From a61fd04ac8f682f1021688f22948bffc45155e5d Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 15 Oct 2023 14:59:09 +0200 Subject: [PATCH] refactor(neon): Simplify account handling for API and URL images Signed-off-by: jld3103 --- .../neon/neon/lib/src/widgets/drawer.dart | 2 +- packages/neon/neon/lib/src/widgets/image.dart | 174 ++++++++++++------ .../src/widgets/unified_search_results.dart | 4 +- .../neon/lib/src/widgets/user_avatar.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 +- tool/find-untested-neon-apis.sh | 2 +- 8 files changed, 125 insertions(+), 65 deletions(-) diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index 88c39b14..eb6b6c9a 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -154,7 +154,7 @@ class NeonDrawerHeader extends StatelessWidget { ), ), Flexible( - child: NeonCachedImage.url( + child: NeonUrlImage( url: theme.logo, ), ), diff --git a/packages/neon/neon/lib/src/widgets/image.dart b/packages/neon/neon/lib/src/widgets/image.dart index b1ebec7b..74878ed0 100644 --- a/packages/neon/neon/lib/src/widgets/image.dart +++ b/packages/neon/neon/lib/src/widgets/image.dart @@ -33,42 +33,15 @@ typedef ApiImageDownloader = FutureOr> Func /// fetches. /// /// See: -/// * [NeonApiImage] for an image from the [NextcloudClient] +/// * [NeonApiImage] for an image widget from an Nextcloud API endpoint. +/// * [NeonUrlImage] for an image widget from an arbitrary URL. /// * [NeonImageWrapper] for a wrapping widget for images class NeonCachedImage extends StatefulWidget { - /// Prints the data from [image] to the canvas. - /// - /// The data is not persisted in the cache. - NeonCachedImage({ - required final Uint8List image, - required Key super.key, - this.isSvgHint = false, - this.size, - this.fit, - this.svgColorFilter, - this.errorBuilder, - }) : image = Future.value(image); - - /// Fetches the image from [url]. - /// - /// The image is automatically cached. - NeonCachedImage.url({ - required final String url, - final Account? account, - final Key? key, - this.isSvgHint = false, - this.size, - this.fit, - this.svgColorFilter, - this.errorBuilder, - }) : image = _getImageFromUrl(url, account), - super(key: key ?? Key(url)); - /// Custom image implementation. /// /// It is possible to provide custom [reviver] and [writeCache] functions to /// adjust the caching. - NeonCachedImage.custom({ + NeonCachedImage({ required final ImageDownloader getImage, required final String cacheKey, final CacheReviver? reviver, @@ -116,20 +89,6 @@ class NeonCachedImage extends StatefulWidget { /// {@endtemplate} final ErrorWidgetBuilder? errorBuilder; - static Future _getImageFromUrl(final String url, final Account? account) async { - var uri = Uri.parse(url); - if (account != null) { - uri = account.completeUri(uri); - } - final file = await _cacheManager.getSingleFile( - uri.toString(), - headers: account != null && uri.host == account.serverURL.host && account.client.authentications.isNotEmpty - ? account.client.authentications.first.headers - : null, - ); - return file.readAsBytes(); - } - static Future _customImageGetter( final CacheReviver? checkCache, final ImageDownloader getImage, @@ -232,18 +191,16 @@ class _NeonCachedImageState extends State { ); } -/// Nextcloud API image. -/// -/// Extension for [NeonCachedImage] providing a [NextcloudClient] to the caller -/// to retrieve the image. +/// A widget painting an Image fetched from the Nextcloud API. /// /// See: /// * [NeonCachedImage] for a customized image +/// * [NeonUrlImage] for an image widget from an arbitrary URL. /// * [NeonImageWrapper] for a wrapping widget for images class NeonApiImage extends StatelessWidget { - /// Creates a new Neon API image with the active account. + /// Creates a new Neon API image fetching the image with the currently active account. /// - /// Use [NeonApiImage.custom] to specify fetching the image with a different account. + /// See [NeonApiImage.withAccount] to fetch the image using a specific account. const NeonApiImage({ required this.getImage, required this.cacheKey, @@ -257,10 +214,10 @@ class NeonApiImage extends StatelessWidget { super.key, }) : account = null; - /// Creates a new Neon API image for a given account. + /// Creates a new Neon API image fetching the image with the given [account]. /// - /// Use [NeonApiImage] to fetch the image with the currently active account. - const NeonApiImage.custom({ + /// See [NeonApiImage] to fetch the image using the currently active account. + const NeonApiImage.withAccount({ required this.getImage, required this.cacheKey, required Account this.account, @@ -274,10 +231,9 @@ class NeonApiImage extends StatelessWidget { super.key, }); - /// Optional account to use for the request. + /// The account to use for the request. /// /// Defaults to the currently active account in [AccountsBloc.activeAccount]. - /// Use the [NeonApiImage.custom] constructor to specify a different account. final Account? account; /// Image downloader. @@ -311,7 +267,7 @@ class NeonApiImage extends StatelessWidget { Widget build(final BuildContext context) { final account = this.account ?? NeonProvider.of(context).activeAccount.value!; - return NeonCachedImage.custom( + return NeonCachedImage( getImage: () async { final response = await getImage(account.client); return response.body; @@ -328,13 +284,117 @@ class NeonApiImage extends StatelessWidget { } } +/// A widget painting an Image fetched from an arbitrary URL. +/// +/// See: +/// * [NeonCachedImage] for a customized image +/// * [NeonApiImage] for an image widget from an Nextcloud API endpoint. +/// * [NeonImageWrapper] for a wrapping widget for images +class NeonUrlImage extends StatelessWidget { + /// Creates a new Neon URL image with the active account. + /// + /// See [NeonUrlImage.withAccount] for using a specific account. + const NeonUrlImage({ + required this.url, + this.reviver, + this.writeCache, + this.isSvgHint = false, + this.size, + this.fit, + this.svgColorFilter, + this.errorBuilder, + super.key, + }) : account = null; + + /// Creates a new Neon URL image with the given [account]. + /// + /// See [NeonUrlImage] for using the active account. + const NeonUrlImage.withAccount({ + required this.url, + required Account this.account, + this.reviver, + this.writeCache, + this.isSvgHint = false, + this.size, + this.fit, + this.svgColorFilter, + this.errorBuilder, + super.key, + }); + + /// The account to use for the request. + /// + /// Defaults to the currently active account in [AccountsBloc.activeAccount]. + final Account? account; + + /// Image url. + final String url; + + /// Custom cache reviver function. + final CacheReviver? reviver; + + /// Custom cache writer function. + final CacheWriter? writeCache; + + /// {@macro NeonCachedImage.svgHint} + final bool isSvgHint; + + /// {@macro NeonCachedImage.size} + final Size? size; + + /// {@macro NeonCachedImage.fit} + final BoxFit? fit; + + /// {@macro NeonCachedImage.svgColorFilter} + final ColorFilter? svgColorFilter; + + /// {@macro NeonCachedImage.errorBuilder} + final ErrorWidgetBuilder? errorBuilder; + + @override + Widget build(final BuildContext context) { + final account = this.account ?? NeonProvider.of(context).activeAccount.value!; + + return NeonCachedImage( + getImage: () async { + final uri = account.completeUri(Uri.parse(url)); + final headers = {}; + + // Only send the authentication headers when sending the request to the server of the account + if (uri.toString().startsWith(account.serverURL.toString()) && account.client.authentications.isNotEmpty) { + headers.addAll(account.client.authentications.first.headers); + } + + final response = await account.client.executeRawRequest( + 'GET', + uri, + headers, + null, + const {200}, + ); + + return response.bytes; + }, + cacheKey: '${account.id}-$url', + reviver: reviver, + writeCache: writeCache, + isSvgHint: isSvgHint, + size: size, + fit: fit, + svgColorFilter: svgColorFilter, + errorBuilder: errorBuilder, + ); + } +} + /// Nextcloud image wrapper widget. /// /// Wraps a child (most commonly an image) into a uniformly styled container. /// /// See: /// * [NeonCachedImage] for a customized image -/// * [NeonApiImage] for an image widget from a [NextcloudClient]. +/// * [NeonApiImage] for an image widget from an Nextcloud API endpoint. +/// * [NeonUrlImage] for an image widget from an arbitrary URL. class NeonImageWrapper extends StatelessWidget { /// Creates a new image wrapper. const NeonImageWrapper({ diff --git a/packages/neon/neon/lib/src/widgets/unified_search_results.dart b/packages/neon/neon/lib/src/widgets/unified_search_results.dart index d6e193c5..166f3c2e 100644 --- a/packages/neon/neon/lib/src/widgets/unified_search_results.dart +++ b/packages/neon/neon/lib/src/widgets/unified_search_results.dart @@ -110,7 +110,7 @@ class NeonUnifiedSearchResults extends StatelessWidget { Widget _buildThumbnail(final BuildContext context, final Account account, final core.UnifiedSearchResultEntry entry) { if (entry.thumbnailUrl.isNotEmpty) { - return NeonCachedImage.url( + return NeonUrlImage.withAccount( size: const Size.square(largeIconSize), url: entry.thumbnailUrl, account: account, @@ -128,7 +128,7 @@ class NeonUnifiedSearchResults extends StatelessWidget { final core.UnifiedSearchResultEntry entry, ) { if (entry.icon.startsWith('/')) { - return NeonCachedImage.url( + return NeonUrlImage.withAccount( size: Size.square(IconTheme.of(context).size!), url: entry.icon, account: account, diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index ca320b4f..c9877480 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -59,7 +59,7 @@ class _UserAvatarState extends State { radius: size / 2, backgroundColor: widget.backgroundColor, child: ClipOval( - child: NeonApiImage.custom( + child: NeonApiImage.withAccount( account: widget.account, cacheKey: 'avatar-${widget.username}-$brightness$pixelSize', getImage: (final client) async => switch (brightness) { diff --git a/packages/neon/neon_news/lib/widgets/articles_view.dart b/packages/neon/neon_news/lib/widgets/articles_view.dart index 358eac44..2132dc2b 100644 --- a/packages/neon/neon_news/lib/widgets/articles_view.dart +++ b/packages/neon/neon_news/lib/widgets/articles_view.dart @@ -117,7 +117,7 @@ class _NewsArticlesViewState extends State { ), ), if (article.mediaThumbnail != null) ...[ - NeonCachedImage.url( + NeonUrlImage( 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 7c6d120f..c325cccc 100644 --- a/packages/neon/neon_news/lib/widgets/feed_icon.dart +++ b/packages/neon/neon_news/lib/widgets/feed_icon.dart @@ -20,7 +20,7 @@ class NewsFeedIcon extends StatelessWidget { size: Size.square(size), borderRadius: borderRadius, child: faviconLink != null && faviconLink.isNotEmpty - ? NeonCachedImage.url( + ? NeonUrlImage( url: faviconLink, size: Size.square(size), ) diff --git a/packages/neon/neon_notifications/lib/pages/main.dart b/packages/neon/neon_notifications/lib/pages/main.dart index 2e54945d..21c6ee5f 100644 --- a/packages/neon/neon_notifications/lib/pages/main.dart +++ b/packages/neon/neon_notifications/lib/pages/main.dart @@ -81,7 +81,7 @@ class _NotificationsMainPageState extends State { ) : SizedBox.fromSize( size: const Size.square(largeIconSize), - child: NeonCachedImage.url( + child: NeonUrlImage( url: notification.icon!, size: const Size.square(largeIconSize), svgColorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), diff --git a/tool/find-untested-neon-apis.sh b/tool/find-untested-neon-apis.sh index 57dfbfb1..cecb299c 100755 --- a/tool/find-untested-neon-apis.sh +++ b/tool/find-untested-neon-apis.sh @@ -4,7 +4,7 @@ cd "$(dirname "$0")/.." function find_apis() { path="$1" - grep -r "$path" --include "*\.dart" -e "client\.[^.]*.[^(]*(" -oh | sed "s/^client\.//" | sed "s/($//" | sed "s/Raw$//" | sort | uniq + grep -r "$path" --include "*\.dart" -e "client\.[^.]*.[^(]*(" -oh | sed "s/^client\.//" | sed "s/($//" | sed "s/Raw$//" | grep -v "^executeRawRequest" | sort | uniq } used_apis=("$(find_apis "packages/neon")")