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. 93
      packages/neon/lib/src/apps/files/widgets/file_preview.dart
  3. 2
      packages/neon/lib/src/neon.dart
  4. 143
      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. 90
      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/apps/files/blocs/files.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart'; import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;

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

@ -32,81 +32,26 @@ class FilePreview extends StatelessWidget {
child: StreamBuilder<bool?>( child: StreamBuilder<bool?>(
stream: bloc.options.showPreviewsOption.stream, stream: bloc.options.showPreviewsOption.stream,
builder: (final context, final showPreviewsSnapshot) { builder: (final context, final showPreviewsSnapshot) {
if (!showPreviewsSnapshot.hasData) { if ((showPreviewsSnapshot.data ?? false) && (details.hasPreview ?? false)) {
return Container(); final account = RxBlocProvider.of<AccountsBloc>(context).activeAccount.value!;
} final child = CachedAPIImage(
if (showPreviewsSnapshot.data! && (details.hasPreview ?? false) && details.etag != null) { account: account,
return Builder( cacheKey: 'preview-${details.path.join('/')}-$width-$height',
builder: (final context) { etag: details.etag,
final account = RxBlocProvider.of<AccountsBloc>(context).activeAccount.value; download: () async => account.client.core.getPreview(
if (account == null) { file: details.path.join('/'),
return Container(); x: width,
} y: height,
),
final stream = Provider.of<RequestManager>(context).wrapBytes(
account.client.id,
'files-preview-${details.etag}-$width-$height',
() 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(
color: Colors.white,
borderRadius: borderRadius,
child: child,
);
}
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 (withBackground) {
return ImageWrapper(
color: Colors.white,
borderRadius: borderRadius,
child: child,
);
}
return child;
} }
if (details.isDirectory) { if (details.isDirectory) {

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

@ -88,6 +88,8 @@ part 'utils/theme.dart';
part 'utils/validators.dart'; part 'utils/validators.dart';
part 'widgets/account_avatar.dart'; part 'widgets/account_avatar.dart';
part 'widgets/account_tile.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/cached_url_image.dart';
part 'widgets/custom_dialog.dart'; part 'widgets/custom_dialog.dart';
part 'widgets/custom_linear_progress_indicator.dart'; part 'widgets/custom_linear_progress_indicator.dart';

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

@ -11,96 +11,73 @@ class AccountAvatar extends StatelessWidget {
final Account account; final Account account;
@override @override
Widget build(final BuildContext context) => Stack( Widget build(final BuildContext context) {
alignment: Alignment.center, final size = (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt();
children: [ return Stack(
ResultStreamBuilder<Uint8List>( alignment: Alignment.center,
// TODO: See TODO in cached_url_image.dart children: [
stream: Provider.of<RequestManager>(context, listen: false).wrapBytes( CircleAvatar(
account.client.id, radius: kAvatarSize / 2,
'accounts-avatar-${account.id}', child: ClipOval(
() async => account.client.core.getAvatar( child: CachedAPIImage(
account: account,
cacheKey: 'avatar-${account.id}-$size',
download: () async => account.client.core.getAvatar(
userId: account.username, userId: account.username,
size: (kAvatarSize * MediaQuery.of(context).devicePixelRatio).toInt(), size: size,
), ),
preferCache: true,
),
builder: (
final context,
final avatarData,
final avatarError,
final avatarLoading,
) =>
Stack(
children: [
if (avatarData != null) ...[
CircleAvatar(
radius: kAvatarSize / 2,
backgroundImage: MemoryImage(avatarData),
),
],
if (avatarError != null) ...[
Icon(
Icons.error_outline,
size: 30,
color: Theme.of(context).colorScheme.onBackground,
),
],
if (avatarLoading) ...[
const CustomLinearProgressIndicator(),
],
],
), ),
), ),
StandardRxResultBuilder<UserStatusBloc, UserStatus?>( ),
bloc: RxBlocProvider.of<AccountsBloc>(context).getUserStatusBloc(account), StandardRxResultBuilder<UserStatusBloc, UserStatus?>(
state: (final bloc) => bloc.userStatus, bloc: RxBlocProvider.of<AccountsBloc>(context).getUserStatusBloc(account),
builder: ( state: (final bloc) => bloc.userStatus,
final context, builder: (
final userStatusData, final context,
final userStatusError, final userStatusData,
final userStatusLoading, final userStatusError,
final _, final userStatusLoading,
) => final _,
SizedBox( ) =>
height: kAvatarSize, SizedBox(
width: kAvatarSize, height: kAvatarSize,
child: Align( width: kAvatarSize,
alignment: Alignment.bottomRight, child: Align(
child: Container( alignment: Alignment.bottomRight,
height: kAvatarSize / 3, child: Container(
width: kAvatarSize / 3, height: kAvatarSize / 3,
decoration: userStatusLoading || userStatusError != null || userStatusData == null width: kAvatarSize / 3,
? null decoration: userStatusLoading || userStatusError != null || userStatusData == null
: BoxDecoration( ? null
shape: BoxShape.circle, : BoxDecoration(
color: _userStatusToColor(userStatusData), shape: BoxShape.circle,
border: userStatusData.status != UserStatusType.offline && color: _userStatusToColor(userStatusData),
userStatusData.status != UserStatusType.invisible border: userStatusData.status != UserStatusType.offline &&
? Border.all( userStatusData.status != UserStatusType.invisible
color: Theme.of(context).colorScheme.onPrimary, ? Border.all(
) color: Theme.of(context).colorScheme.onPrimary,
: null, )
), : null,
child: userStatusLoading ),
? CircularProgressIndicator( child: userStatusLoading
strokeWidth: 1.5, ? CircularProgressIndicator(
color: Theme.of(context).colorScheme.onPrimary, 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( ? const Icon(
Icons.error_outline, Icons.error_outline,
size: kAvatarSize / 3, size: kAvatarSize / 3,
color: Colors.red, color: Colors.red,
) )
: null, : null,
),
), ),
), ),
), ),
], ),
); ],
);
}
Color _userStatusToColor(final UserStatus userStatus) { Color _userStatusToColor(final UserStatus userStatus) {
switch (userStatus.status) { 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,
),
);
},
);
}

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

@ -1,82 +1,16 @@
part of '../neon.dart'; part of '../neon.dart';
final _cacheManager = DefaultCacheManager(); class CachedURLImage extends CachedImage {
CachedURLImage({
class CachedURLImage extends StatelessWidget { required final String url,
const CachedURLImage({ super.height,
required this.url, super.width,
this.height, super.fit,
this.width, super.svgColor,
this.fit, super.iconColor,
this.svgColor,
this.iconColor,
super.key, super.key,
}); }) : super(
future: _cacheManager.getSingleFile(url),
final String url; isSvgHint: Uri.parse(url).path.endsWith('.svg'),
);
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
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,
),
);
},
);
} }

Loading…
Cancel
Save