Browse Source

Merge pull request #78 from jld3103/fix/api-images

neon: Fix API images
pull/79/head
jld3103 2 years ago committed by GitHub
parent
commit
fc97195425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/neon/lib/src/apps/files/app.dart
  2. 69
      packages/neon/lib/src/apps/files/widgets/file_preview.dart
  3. 2
      packages/neon/lib/src/neon.dart
  4. 47
      packages/neon/lib/src/widgets/account_avatar.dart
  5. 33
      packages/neon/lib/src/widgets/cached_api_image.dart
  6. 72
      packages/neon/lib/src/widgets/cached_image.dart
  7. 86
      packages/neon/lib/src/widgets/cached_url_image.dart

1
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;

69
packages/neon/lib/src/apps/files/widgets/file_preview.dart

@ -32,47 +32,17 @@ class FilePreview extends StatelessWidget {
child: StreamBuilder<bool?>(
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<AccountsBloc>(context).activeAccount.value;
if (account == null) {
return Container();
}
final stream = Provider.of<RequestManager>(context).wrapBytes(
account.client.id,
'files-preview-${details.etag}-$width-$height',
() async => account.client.core.getPreview(
if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) {
final account = RxBlocProvider.of<AccountsBloc>(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,
),
preferCache: true,
);
return ResultStreamBuilder<Uint8List>(
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(
@ -82,31 +52,6 @@ class FilePreview extends StatelessWidget {
);
}
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 (details.isDirectory) {

2
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';

47
packages/neon/lib/src/widgets/account_avatar.dart

@ -11,45 +11,22 @@ class AccountAvatar extends StatelessWidget {
final Account account;
@override
Widget build(final BuildContext context) => Stack(
Widget build(final BuildContext context) {
final size = (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt();
return Stack(
alignment: Alignment.center,
children: [
ResultStreamBuilder<Uint8List>(
// TODO: See TODO in cached_url_image.dart
stream: Provider.of<RequestManager>(context, listen: false).wrapBytes(
account.client.id,
'accounts-avatar-${account.id}',
() async => account.client.core.getAvatar(
userId: account.username,
size: (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt(),
),
preferCache: true,
),
builder: (
final context,
final avatarData,
final avatarError,
final avatarLoading,
) =>
Stack(
children: [
if (avatarData != null) ...[
CircleAvatar(
radius: kAvatarSize / 2,
backgroundImage: MemoryImage(avatarData),
child: ClipOval(
child: CachedAPIImage(
account: account,
cacheKey: 'avatar-${account.id}-$size',
download: () async => account.client.core.getAvatar(
userId: account.username,
size: size,
),
],
if (avatarError != null) ...[
Icon(
Icons.error_outline,
size: 30,
color: Theme.of(context).colorScheme.onBackground,
),
],
if (avatarLoading) ...[
const CustomLinearProgressIndicator(),
],
],
),
),
StandardRxResultBuilder<UserStatusBloc, UserStatus?>(
@ -87,8 +64,7 @@ class AccountAvatar extends StatelessWidget {
strokeWidth: 1.5,
color: Theme.of(context).colorScheme.onPrimary,
)
: userStatusError != null &&
(userStatusError is! ApiException || userStatusError.statusCode != 404)
: userStatusError != null && (userStatusError is! ApiException || userStatusError.statusCode != 404)
? const Icon(
Icons.error_outline,
size: kAvatarSize / 3,
@ -101,6 +77,7 @@ class AccountAvatar extends StatelessWidget {
),
],
);
}
Color _userStatusToColor(final UserStatus userStatus) {
switch (userStatus.status) {

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

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

72
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<File> 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<File>(
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('<svg')) {
return SvgPicture.memory(
content,
height: height,
width: width,
fit: fit ?? BoxFit.contain,
color: svgColor,
);
}
} catch (_) {
// If the data is not UTF-8
}
return Image.memory(
content,
height: height,
width: width,
fit: fit,
gaplessPlayback: true,
);
}
if (fileSnapshot.hasError) {
return Icon(
Icons.error_outline,
size: height != null && width != null ? min(height!, width!) : height ?? width,
color: iconColor,
);
}
return SizedBox(
width: width,
child: CustomLinearProgressIndicator(
color: iconColor,
),
);
},
);
}

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

@ -1,82 +1,16 @@
part of '../neon.dart';
final _cacheManager = DefaultCacheManager();
class CachedURLImage extends StatelessWidget {
const CachedURLImage({
required this.url,
this.height,
this.width,
this.fit,
this.svgColor,
this.iconColor,
class CachedURLImage extends CachedImage {
CachedURLImage({
required final String url,
super.height,
super.width,
super.fit,
super.svgColor,
super.iconColor,
super.key,
});
final String url;
final double? height;
final double? width;
final BoxFit? fit;
final Color? svgColor;
final Color? iconColor;
@override
Widget build(final BuildContext context) => FutureBuilder<File>(
// Really weird false positive
// ignore: discarded_futures
}) : super(
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('<svg')) {
isSvg = true;
}
} catch (_) {
// If the data is not UTF-8
}
}
if (isSvg) {
return SvgPicture.memory(
content,
height: height,
width: width,
fit: fit ?? BoxFit.contain,
color: svgColor,
);
}
return Image.memory(
content,
height: height,
width: width,
fit: fit,
gaplessPlayback: true,
);
}
if (fileSnapshot.hasError) {
return Icon(
Icons.error_outline,
size: height != null && width != null ? min(height!, width!) : height ?? width,
color: iconColor,
);
}
return SizedBox(
width: width,
child: CustomLinearProgressIndicator(
color: iconColor,
),
);
},
isSvgHint: Uri.parse(url).path.endsWith('.svg'),
);
}

Loading…
Cancel
Save