Browse Source

neon, neon_files: make NeonCachedImage easily extendable

The new FilePreviewImage now also uses the mimetype as as svgHint
pull/432/head
Nikolas Rimikis 1 year ago
parent
commit
4f0009939e
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 142
      packages/neon/neon/lib/src/widgets/cached_image.dart
  2. 11
      packages/neon/neon/lib/src/widgets/user_avatar.dart
  3. 89
      packages/neon/neon_files/lib/widgets/file_preview.dart

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

@ -1,12 +1,12 @@
part of '../../neon.dart'; part of '../../neon.dart';
final _cacheManager = DefaultCacheManager(); typedef CacheReviver = FutureOr<Uint8List?> Function(CacheManager cacheManager);
typedef ImageDownloader = FutureOr<Uint8List> Function();
typedef APIImageDownloader = Future<Uint8List> Function(); typedef CacheWriter = Future<void> Function(CacheManager cacheManager, Uint8List image);
class NeonCachedImage extends StatefulWidget { class NeonCachedImage extends StatefulWidget {
const NeonCachedImage._({ const NeonCachedImage({
required this.getImageFile, required this.image,
required Key super.key, required Key super.key,
this.isSvgHint = false, this.isSvgHint = false,
this.size, this.size,
@ -15,87 +15,98 @@ class NeonCachedImage extends StatefulWidget {
this.iconColor, this.iconColor,
}); });
factory NeonCachedImage.url({ NeonCachedImage.url({
required final String url, required final String url,
final Size? size,
final BoxFit? fit,
final Color? svgColor,
final Color? iconColor,
final Key? key, final Key? key,
}) => this.isSvgHint = false,
NeonCachedImage._( this.size,
getImageFile: () async { 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 Size? size;
final BoxFit? fit;
final Color? svgColor;
final Color? iconColor;
static Future<Uint8List> _getImageFromUrl(final String url) async {
final file = await _cacheManager.getSingleFile(url); final file = await _cacheManager.getSingleFile(url);
return file.readAsBytes(); return file.readAsBytes();
}, }
isSvgHint: Uri.parse(url).path.endsWith('.svg'),
size: size,
fit: fit,
svgColor: svgColor,
iconColor: iconColor,
key: key ?? Key(url),
);
factory NeonCachedImage.api({ static Future<Uint8List> _customImageGetter(
required final Account account, final CacheReviver? checkCache,
required final String cacheKey, final ImageDownloader getImage,
required final APIImageDownloader download, final CacheWriter? writeCache,
final String? etag, final String cacheKey,
final Size? size, ) async {
final BoxFit? fit, final cached = await checkCache?.call(_cacheManager) ?? await _defaultCacheReviver(cacheKey);
final Color? svgColor, if (cached != null) {
final Color? iconColor, return cached;
final Key? key, }
}) {
final realKey = '${account.id}-$cacheKey'; final data = await getImage();
return NeonCachedImage._(
getImageFile: () async { unawaited(writeCache?.call(_cacheManager, data) ?? _defaultCacheWriter(data, cacheKey));
final cacheFile = await _cacheManager.getFileFromCache(realKey);
return data;
}
static Future<Uint8List?> _defaultCacheReviver(final String cacheKey) async {
final cacheFile = await _cacheManager.getFileFromCache(cacheKey);
if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
return cacheFile.file.readAsBytes(); return cacheFile.file.readAsBytes();
} }
final file = await download(); return null;
}
unawaited( static Future<void> _defaultCacheWriter(
_cacheManager.putFile( final Uint8List data,
realKey, final String cacheKey,
file, ) async {
await _cacheManager.putFile(
cacheKey,
data,
maxAge: const Duration(days: 7), maxAge: const Duration(days: 7),
eTag: etag,
),
);
return file;
},
size: size,
fit: fit,
svgColor: svgColor,
iconColor: iconColor,
key: key ?? Key(realKey),
); );
} }
final Future<Uint8List> Function() getImageFile; static final _cacheManager = DefaultCacheManager();
final bool isSvgHint;
final Size? size;
final BoxFit? fit;
final Color? svgColor;
final Color? iconColor;
@override @override
State<NeonCachedImage> createState() => _NeonCachedImageState(); State<NeonCachedImage> createState() => _NeonCachedImageState();
} }
class _NeonCachedImageState extends State<NeonCachedImage> { class _NeonCachedImageState extends State<NeonCachedImage> {
late Future<Uint8List> _future = widget.getImageFile();
@override @override
Widget build(final BuildContext context) => Center( Widget build(final BuildContext context) => Center(
child: FutureBuilder<Uint8List>( child: FutureBuilder<Uint8List>(
future: _future, future: widget.image,
builder: (final context, final fileSnapshot) { builder: (final context, final fileSnapshot) {
if (!fileSnapshot.hasData) { if (!fileSnapshot.hasData) {
return SizedBox( return SizedBox(
@ -110,10 +121,7 @@ class _NeonCachedImageState extends State<NeonCachedImage> {
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,

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: NeonCachedImage.api( 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,

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

@ -21,28 +21,31 @@ 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) {
if (details.isDirectory) {
return Icon(
MdiIcons.folder,
color: color,
size: size.shortestSide,
);
}
return OptionBuilder<bool>(
option: bloc.options.showPreviewsOption,
builder: (final context, final showPreviewsSnapshot) { builder: (final context, final showPreviewsSnapshot) {
if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) { if (showPreviewsSnapshot && (details.hasPreview ?? false)) {
final account = Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!; final account = Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!;
final child = NeonCachedImage.api( final child = FilePreviewImage(
account: account, account: account,
cacheKey: 'preview-${details.path.join('/')}-$width-$height', file: details,
etag: details.etag, size: size,
download: () async => account.client.core.getPreview(
file: details.path.join('/'),
x: width,
y: height,
),
); );
if (withBackground) { if (withBackground) {
return NeonImageWrapper( return NeonImageWrapper(
@ -54,21 +57,63 @@ class FilePreview extends StatelessWidget {
return child; return child;
} }
if (details.isDirectory) {
return Icon(
MdiIcons.folder,
color: color,
size: size.shortestSide,
);
}
return FileIcon( return FileIcon(
details.name, details.name,
color: color, color: color,
size: size.shortestSide, 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,
);
}

Loading…
Cancel
Save