Browse Source

Merge pull request #432 from Leptopoda/fix/list_cached_image

Fix/list cached image
pull/440/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
46d64f3dc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/neon/neon/lib/neon.dart
  2. 32
      packages/neon/neon/lib/src/widgets/cached_api_image.dart
  3. 153
      packages/neon/neon/lib/src/widgets/cached_image.dart
  4. 15
      packages/neon/neon/lib/src/widgets/cached_url_image.dart
  5. 2
      packages/neon/neon/lib/src/widgets/drawer.dart
  6. 11
      packages/neon/neon/lib/src/widgets/user_avatar.dart
  7. 109
      packages/neon/neon_files/lib/widgets/file_preview.dart
  8. 2
      packages/neon/neon_news/lib/widgets/articles_view.dart
  9. 2
      packages/neon/neon_news/lib/widgets/feed_icon.dart
  10. 2
      packages/neon/neon_notifications/lib/pages/main.dart

2
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_settings_tile.dart';
part 'src/widgets/account_tile.dart'; part 'src/widgets/account_tile.dart';
part 'src/widgets/app_implementation_icon.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_image.dart';
part 'src/widgets/cached_url_image.dart';
part 'src/widgets/dialog.dart'; part 'src/widgets/dialog.dart';
part 'src/widgets/exception.dart'; part 'src/widgets/exception.dart';
part 'src/widgets/image_wrapper.dart'; part 'src/widgets/image_wrapper.dart';

32
packages/neon/neon/lib/src/widgets/cached_api_image.dart

@ -1,32 +0,0 @@
part of '../../neon.dart';
typedef APIImageDownloader = Future<Uint8List> 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,
);
},
);
}

153
packages/neon/neon/lib/src/widgets/cached_image.dart

@ -1,19 +1,50 @@
part of '../../neon.dart'; part of '../../neon.dart';
final _cacheManager = DefaultCacheManager(); typedef CacheReviver = FutureOr<Uint8List?> Function(CacheManager cacheManager);
typedef ImageDownloader = FutureOr<Uint8List> Function();
typedef CacheWriter = Future<void> Function(CacheManager cacheManager, Uint8List image);
abstract class NeonCachedImage extends StatefulWidget { class NeonCachedImage extends StatefulWidget {
const NeonCachedImage({ const NeonCachedImage({
required this.getImageFile, required this.image,
required Key super.key,
this.isSvgHint = false, this.isSvgHint = false,
this.size, this.size,
this.fit, this.fit,
this.svgColor, this.svgColor,
this.iconColor, this.iconColor,
super.key,
}); });
final Future<File> 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<Uint8List> image;
final bool isSvgHint; final bool isSvgHint;
final Size? size; final Size? size;
@ -22,63 +53,105 @@ abstract class NeonCachedImage extends StatefulWidget {
final Color? svgColor; final Color? svgColor;
final Color? iconColor; final Color? iconColor;
static Future<Uint8List> _getImageFromUrl(final String url) async {
final file = await _cacheManager.getSingleFile(url);
return file.readAsBytes();
}
static Future<Uint8List> _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<Uint8List?> _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<void> _defaultCacheWriter(
final Uint8List data,
final String cacheKey,
) async {
await _cacheManager.putFile(
cacheKey,
data,
maxAge: const Duration(days: 7),
);
}
static final _cacheManager = DefaultCacheManager();
@override @override
State<NeonCachedImage> createState() => _NeonCachedImageState(); State<NeonCachedImage> createState() => _NeonCachedImageState();
} }
class _NeonCachedImageState extends State<NeonCachedImage> { class _NeonCachedImageState extends State<NeonCachedImage> {
late Future<File> future = widget.getImageFile();
@override @override
Widget build(final BuildContext context) => Center( Widget build(final BuildContext context) => Center(
child: FutureBuilder<File>( child: FutureBuilder<Uint8List>(
future: future, future: widget.image,
builder: (final context, final fileSnapshot) { builder: (final context, final fileSnapshot) {
if (fileSnapshot.hasData) { if (!fileSnapshot.hasData) {
final content = fileSnapshot.requireData.readAsBytesSync(); return SizedBox(
try {
// TODO: Is this safe enough?
if (widget.isSvgHint || utf8.decode(content).contains('<svg')) {
return SvgPicture.memory(
content,
height: widget.size?.height,
width: widget.size?.width,
fit: widget.fit ?? BoxFit.contain,
colorFilter: widget.svgColor != null ? ColorFilter.mode(widget.svgColor!, BlendMode.srcIn) : null,
);
}
} catch (_) {
// If the data is not UTF-8
}
return Image.memory(
content,
height: widget.size?.height,
width: widget.size?.width, width: widget.size?.width,
fit: widget.fit, child: NeonLinearProgressIndicator(
gaplessPlayback: true, color: widget.iconColor,
),
); );
} }
if (fileSnapshot.hasError) { if (fileSnapshot.hasError) {
return NeonException( return NeonException(
fileSnapshot.error, fileSnapshot.error,
onRetry: () { onRetry: () {
setState(() { setState(() {});
// ignore: discarded_futures
future = widget.getImageFile();
});
}, },
onlyIcon: true, onlyIcon: true,
iconSize: widget.size?.shortestSide, iconSize: widget.size?.shortestSide,
color: widget.iconColor ?? Theme.of(context).colorScheme.error, 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('<svg')) {
return SvgPicture.memory(
content,
height: widget.size?.height,
width: widget.size?.width,
fit: widget.fit ?? BoxFit.contain,
colorFilter: widget.svgColor != null ? ColorFilter.mode(widget.svgColor!, BlendMode.srcIn) : null,
);
}
} catch (_) {
// If the data is not UTF-8
}
return Image.memory(
content,
height: widget.size?.height,
width: widget.size?.width, width: widget.size?.width,
child: NeonLinearProgressIndicator( fit: widget.fit,
color: widget.iconColor, gaplessPlayback: true,
),
); );
}, },
), ),

15
packages/neon/neon/lib/src/widgets/cached_url_image.dart

@ -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'),
);
}

2
packages/neon/neon/lib/src/widgets/drawer.dart

@ -187,7 +187,7 @@ class NeonDrawerHeader extends StatelessWidget {
], ],
if (capabilities.requireData.capabilities.theming?.logo != null) ...[ if (capabilities.requireData.capabilities.theming?.logo != null) ...[
Flexible( Flexible(
child: NeonCachedUrlImage( child: NeonCachedImage.url(
url: capabilities.requireData.capabilities.theming!.logo!, url: capabilities.requireData.capabilities.theming!.logo!,
), ),
), ),

11
packages/neon/neon/lib/src/widgets/user_avatar.dart

@ -43,7 +43,7 @@ class _UserAvatarState extends State<NeonUserAvatar> {
@override @override
Widget build(final BuildContext context) => LayoutBuilder( Widget build(final BuildContext context) => LayoutBuilder(
builder: (final context, final constraints) { 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; size = constraints.constrain(Size.square(widget.size)).shortestSide;
final pixelSize = (size * MediaQuery.of(context).devicePixelRatio).toInt(); final pixelSize = (size * MediaQuery.of(context).devicePixelRatio).toInt();
return Stack( return Stack(
@ -53,11 +53,10 @@ class _UserAvatarState extends State<NeonUserAvatar> {
radius: size / 2, radius: size / 2,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
child: ClipOval( child: ClipOval(
child: NeonCachedApiImage( child: NeonCachedImage.custom(
account: widget.account, cacheKey: '${widget.account.id}-avatar-${widget.username}-$brightness$pixelSize',
cacheKey: 'avatar-${widget.username}-${isDark ? 'dark' : 'light'}$pixelSize', getImage: () async {
download: () async { if (brightness == Brightness.dark) {
if (isDark) {
return widget.account.client.core.getDarkAvatar( return widget.account.client.core.getDarkAvatar(
userId: widget.username, userId: widget.username,
size: pixelSize, size: pixelSize,

109
packages/neon/neon_files/lib/widgets/file_preview.dart

@ -21,39 +21,14 @@ class FilePreview extends StatelessWidget {
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
final bool withBackground; final bool withBackground;
int get width => size.width.toInt();
int get height => size.height.toInt();
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final color = this.color ?? Theme.of(context).colorScheme.primary; final color = this.color ?? Theme.of(context).colorScheme.primary;
return SizedBox.fromSize( return SizedBox.fromSize(
size: size, size: size,
child: StreamBuilder<bool?>( child: Builder(
stream: bloc.options.showPreviewsOption.stream, builder: (final context) {
builder: (final context, final showPreviewsSnapshot) {
if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) {
final account = Provider.of<AccountsBloc>(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;
}
if (details.isDirectory) { if (details.isDirectory) {
return Icon( return Icon(
MdiIcons.folder, MdiIcons.folder,
@ -62,13 +37,83 @@ class FilePreview extends StatelessWidget {
); );
} }
return FileIcon( return OptionBuilder<bool>(
details.name, option: bloc.options.showPreviewsOption,
color: color, builder: (final context, final showPreviewsSnapshot) {
size: size.shortestSide, if (showPreviewsSnapshot && (details.hasPreview ?? false)) {
final account = Provider.of<AccountsBloc>(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,
);
}

2
packages/neon/neon_news/lib/widgets/articles_view.dart

@ -116,7 +116,7 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
), ),
), ),
if (article.mediaThumbnail != null) ...[ if (article.mediaThumbnail != null) ...[
NeonCachedUrlImage( NeonCachedImage.url(
url: article.mediaThumbnail!, url: article.mediaThumbnail!,
size: const Size(100, 50), size: const Size(100, 50),
fit: BoxFit.cover, fit: BoxFit.cover,

2
packages/neon/neon_news/lib/widgets/feed_icon.dart

@ -21,7 +21,7 @@ class NewsFeedIcon extends StatelessWidget {
size: Size.square(size), size: Size.square(size),
borderRadius: borderRadius, borderRadius: borderRadius,
child: faviconLink != null && faviconLink.isNotEmpty child: faviconLink != null && faviconLink.isNotEmpty
? NeonCachedUrlImage( ? NeonCachedImage.url(
url: faviconLink, url: faviconLink,
size: Size.square(size), size: Size.square(size),
iconColor: Theme.of(context).colorScheme.primary, iconColor: Theme.of(context).colorScheme.primary,

2
packages/neon/neon_notifications/lib/pages/main.dart

@ -78,7 +78,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
) )
: SizedBox.fromSize( : SizedBox.fromSize(
size: const Size.square(40), size: const Size.square(40),
child: NeonCachedUrlImage( child: NeonCachedImage.url(
url: notification.icon!, url: notification.icon!,
size: const Size.square(40), size: const Size.square(40),
svgColor: Theme.of(context).colorScheme.primary, svgColor: Theme.of(context).colorScheme.primary,

Loading…
Cancel
Save