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. 146
      packages/neon/neon/lib/src/widgets/cached_image.dart
  2. 11
      packages/neon/neon/lib/src/widgets/user_avatar.dart
  3. 109
      packages/neon/neon_files/lib/widgets/file_preview.dart

146
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,68 +15,36 @@ 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,
final file = await _cacheManager.getSingleFile(url); this.svgColor,
return file.readAsBytes(); this.iconColor,
}, }) : image = _getImageFromUrl(url),
isSvgHint: Uri.parse(url).path.endsWith('.svg'), super(key: key ?? Key(url));
size: size,
fit: fit,
svgColor: svgColor,
iconColor: iconColor,
key: key ?? Key(url),
);
factory NeonCachedImage.api({ NeonCachedImage.custom({
required final Account account, required final ImageDownloader getImage,
required final String cacheKey, required final String cacheKey,
required final APIImageDownloader download, final CacheReviver? reviver,
final String? etag, final CacheWriter? writeCache,
final Size? size, this.isSvgHint = false,
final BoxFit? fit, this.size,
final Color? svgColor, this.fit,
final Color? iconColor, this.svgColor,
final Key? key, this.iconColor,
}) { }) : image = _customImageGetter(
final realKey = '${account.id}-$cacheKey'; reviver,
return NeonCachedImage._( getImage,
getImageFile: () async { writeCache,
final cacheFile = await _cacheManager.getFileFromCache(realKey); cacheKey,
if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) { ),
return cacheFile.file.readAsBytes(); super(key: Key(cacheKey));
}
final file = await download();
unawaited(
_cacheManager.putFile(
realKey,
file,
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; final Future<Uint8List> image;
final bool isSvgHint; final bool isSvgHint;
final Size? size; final Size? size;
@ -85,17 +53,60 @@ 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<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,

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 = NeonCachedImage.api(
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,
);
}

Loading…
Cancel
Save