mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-23 20:49:26 +01:00
Merge branch 'krille/simplify-mxc-content' into 'main'
refactor: Simplify MxcImage and replace CachedNetworkImage See merge request famedly/fluffychat!967
This commit is contained in:
commit
d20c74c298
@ -1,20 +1,13 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:lottie/lottie.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pages/image_viewer/image_viewer.dart';
|
import 'package:fluffychat/pages/image_viewer/image_viewer.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import '../../../utils/matrix_sdk_extensions.dart/event_extension.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
|
|
||||||
class ImageBubble extends StatefulWidget {
|
class ImageBubble extends StatelessWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
final bool tapToView;
|
final bool tapToView;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
@ -24,7 +17,6 @@ class ImageBubble extends StatefulWidget {
|
|||||||
final bool animated;
|
final bool animated;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final void Function()? onLoaded;
|
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
|
|
||||||
const ImageBubble(
|
const ImageBubble(
|
||||||
@ -34,7 +26,6 @@ class ImageBubble extends StatefulWidget {
|
|||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.thumbnailOnly = true,
|
this.thumbnailOnly = true,
|
||||||
this.onLoaded,
|
|
||||||
this.width = 400,
|
this.width = 400,
|
||||||
this.height = 300,
|
this.height = 300,
|
||||||
this.animated = false,
|
this.animated = false,
|
||||||
@ -42,446 +33,78 @@ class ImageBubble extends StatefulWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
Widget _buildPlaceholder(BuildContext context) {
|
||||||
_ImageBubbleState createState() => _ImageBubbleState();
|
if (event.messageType == MessageTypes.Sticker) {
|
||||||
}
|
return const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
class _ImageBubbleState extends State<ImageBubble> {
|
);
|
||||||
// for plaintext: holds the http URL for the thumbnail
|
|
||||||
String? thumbnailUrl;
|
|
||||||
// for plaintext. holds the http URL for the thumbnial, without the animated flag
|
|
||||||
String? thumbnailUrlNoAnimated;
|
|
||||||
// for plaintext: holds the http URL of the original
|
|
||||||
String? attachmentUrl;
|
|
||||||
MatrixFile? _file;
|
|
||||||
MatrixFile? _thumbnail;
|
|
||||||
bool _requestedThumbnailOnFailure = false;
|
|
||||||
// In case we have animated = false, this will hold the first frame so that we make
|
|
||||||
// sure that things are never animated
|
|
||||||
Widget? _firstFrame;
|
|
||||||
|
|
||||||
// the mimetypes that we know how to render, from the flutter Image widget
|
|
||||||
final _knownMimetypes = <String>{
|
|
||||||
'image/jpg',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/apng',
|
|
||||||
'image/webp',
|
|
||||||
'image/gif',
|
|
||||||
'image/bmp',
|
|
||||||
'image/x-bmp',
|
|
||||||
};
|
|
||||||
|
|
||||||
// overrides for certain mimetypes if they need different images to render
|
|
||||||
// memory are for in-memory renderers (e2ee rooms), network for network url renderers.
|
|
||||||
// The map values themself are set in initState() as they need to be able to access
|
|
||||||
// `this`.
|
|
||||||
final _contentRenderers = <String, _ImageBubbleContentRenderer>{};
|
|
||||||
|
|
||||||
String getMimetype([bool thumbnail = false]) => thumbnail
|
|
||||||
? widget.event.thumbnailMimetype.toLowerCase()
|
|
||||||
: widget.event.attachmentMimetype.toLowerCase();
|
|
||||||
|
|
||||||
MatrixFile? get _displayFile => _file ?? _thumbnail;
|
|
||||||
String? get displayUrl => widget.thumbnailOnly ? thumbnailUrl : attachmentUrl;
|
|
||||||
|
|
||||||
dynamic _error;
|
|
||||||
|
|
||||||
Future<void> _requestFile({bool getThumbnail = false}) async {
|
|
||||||
try {
|
|
||||||
final res = await widget.event
|
|
||||||
.downloadAndDecryptAttachmentCached(getThumbnail: getThumbnail);
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (getThumbnail) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _thumbnail = res);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (widget.onLoaded != null) {
|
|
||||||
widget.onLoaded!();
|
|
||||||
}
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _file = res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _error = err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
final String blurHashString =
|
||||||
|
event.infoMap['xyz.amorgan.blurhash'] is String
|
||||||
Widget frameBuilder(_, Widget child, int? frame, __) {
|
? event.infoMap['xyz.amorgan.blurhash']
|
||||||
// as servers might return animated gifs as thumbnails and we want them to *not* play
|
: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj';
|
||||||
// animated, we'll have to store the first frame in a variable and display that instead
|
final ratio = event.infoMap['w'] is int && event.infoMap['h'] is int
|
||||||
if (widget.animated) {
|
? event.infoMap['w'] / event.infoMap['h']
|
||||||
return child;
|
: 1.0;
|
||||||
}
|
var width = 32;
|
||||||
if (frame == 0) {
|
var height = 32;
|
||||||
_firstFrame = child;
|
if (ratio > 1.0) {
|
||||||
}
|
height = (width / ratio).round();
|
||||||
return _firstFrame ?? child;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
// add the custom renderers for other mimetypes
|
|
||||||
_contentRenderers['image/svg+xml'] = _ImageBubbleContentRenderer(
|
|
||||||
memory: (Uint8List bytes, String key) => SvgPicture.memory(
|
|
||||||
bytes,
|
|
||||||
key: ValueKey(key),
|
|
||||||
fit: widget.fit,
|
|
||||||
),
|
|
||||||
network: (String? url) => url == null
|
|
||||||
? Container()
|
|
||||||
: SvgPicture.network(
|
|
||||||
url,
|
|
||||||
key: ValueKey(url),
|
|
||||||
placeholderBuilder: (context) => getPlaceholderWidget(),
|
|
||||||
fit: widget.fit,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_contentRenderers['image/lottie+json'] = _ImageBubbleContentRenderer(
|
|
||||||
memory: (Uint8List bytes, String key) => Lottie.memory(
|
|
||||||
bytes,
|
|
||||||
key: ValueKey(key),
|
|
||||||
fit: widget.fit,
|
|
||||||
errorBuilder: (context, error, stacktrace) =>
|
|
||||||
getErrorWidget(context, error),
|
|
||||||
animate: widget.animated,
|
|
||||||
),
|
|
||||||
network: (String? url) => url == null
|
|
||||||
? Container()
|
|
||||||
: Lottie.network(
|
|
||||||
url,
|
|
||||||
key: ValueKey(url),
|
|
||||||
fit: widget.fit,
|
|
||||||
errorBuilder: (context, error, stacktrace) =>
|
|
||||||
getErrorWidget(context, error),
|
|
||||||
animate: widget.animated,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// add all the custom content renderer mimetypes to the known mimetypes set
|
|
||||||
for (final key in _contentRenderers.keys) {
|
|
||||||
_knownMimetypes.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbnailUrl = widget.event
|
|
||||||
.getAttachmentUrl(getThumbnail: true, animated: widget.animated)
|
|
||||||
?.toString();
|
|
||||||
thumbnailUrlNoAnimated = widget.event
|
|
||||||
.getAttachmentUrl(getThumbnail: true, animated: false)
|
|
||||||
?.toString();
|
|
||||||
attachmentUrl =
|
|
||||||
widget.event.getAttachmentUrl(animated: widget.animated)?.toString();
|
|
||||||
if (thumbnailUrl == null) {
|
|
||||||
_requestFile(getThumbnail: true);
|
|
||||||
}
|
|
||||||
if (!widget.thumbnailOnly && attachmentUrl == null) {
|
|
||||||
_requestFile();
|
|
||||||
} else {
|
} else {
|
||||||
// if the full attachment is cached, we might as well fetch it anyways.
|
width = (height * ratio).round();
|
||||||
// no need to stick with thumbnail only, since we don't do any networking
|
|
||||||
widget.event.isAttachmentCached().then((cached) {
|
|
||||||
if (cached) {
|
|
||||||
_requestFile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
super.initState();
|
return SizedBox(
|
||||||
}
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
Widget getErrorWidget(BuildContext context, [dynamic error]) {
|
child: BlurHash(
|
||||||
final String filename = widget.event.content.containsKey('filename')
|
hash: blurHashString,
|
||||||
? widget.event.content['filename']
|
|
||||||
: widget.event.body;
|
|
||||||
return getPlaceholderWidget(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
primary: Theme.of(context).textTheme.bodyText1!.color,
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.download_outlined),
|
|
||||||
onPressed: () => widget.event.saveFile(context),
|
|
||||||
label: Text(
|
|
||||||
filename,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (widget.event.sizeString != null) Text(widget.event.sizeString!),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text((error ?? _error).toString()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget getPlaceholderWidget({Widget? child}) {
|
|
||||||
Widget? blurhash;
|
|
||||||
if (widget.event.infoMap['xyz.amorgan.blurhash'] is String) {
|
|
||||||
final ratio =
|
|
||||||
widget.event.infoMap['w'] is int && widget.event.infoMap['h'] is int
|
|
||||||
? widget.event.infoMap['w'] / widget.event.infoMap['h']
|
|
||||||
: 1.0;
|
|
||||||
var width = 32;
|
|
||||||
var height = 32;
|
|
||||||
if (ratio > 1.0) {
|
|
||||||
height = (width / ratio).round();
|
|
||||||
} else {
|
|
||||||
width = (height * ratio).round();
|
|
||||||
}
|
|
||||||
blurhash = BlurHash(
|
|
||||||
hash: widget.event.infoMap['xyz.amorgan.blurhash'],
|
|
||||||
decodingWidth: width,
|
decodingWidth: width,
|
||||||
decodingHeight: height,
|
decodingHeight: height,
|
||||||
imageFit: widget.fit,
|
imageFit: fit,
|
||||||
);
|
),
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
if (blurhash != null) blurhash,
|
|
||||||
Center(
|
|
||||||
child:
|
|
||||||
child ?? const CircularProgressIndicator.adaptive(strokeWidth: 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a memory file (e2ee)
|
void _onTap(BuildContext context) {
|
||||||
Widget getMemoryWidget() {
|
if (onTap != null) {
|
||||||
final isOriginal = _file != null ||
|
onTap!();
|
||||||
widget.event.attachmentOrThumbnailMxcUrl(getThumbnail: true) ==
|
return;
|
||||||
widget.event.attachmentMxcUrl;
|
|
||||||
final key = isOriginal
|
|
||||||
? widget.event.attachmentMxcUrl.toString()
|
|
||||||
: widget.event.thumbnailMxcUrl.toString();
|
|
||||||
final mimetype = getMimetype(!isOriginal);
|
|
||||||
if (_contentRenderers.containsKey(mimetype)) {
|
|
||||||
return _contentRenderers[mimetype]!.memory!(_displayFile!.bytes, key);
|
|
||||||
} else {
|
|
||||||
return Image.memory(
|
|
||||||
_displayFile!.bytes,
|
|
||||||
key: ValueKey(key),
|
|
||||||
fit: widget.fit,
|
|
||||||
errorBuilder: (context, error, stacktrace) {
|
|
||||||
if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) {
|
|
||||||
_requestedThumbnailOnFailure = true;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
setState(() {
|
|
||||||
_file = null;
|
|
||||||
_requestFile(getThumbnail: true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return getPlaceholderWidget();
|
|
||||||
}
|
|
||||||
return getErrorWidget(context, error);
|
|
||||||
},
|
|
||||||
frameBuilder: frameBuilder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// build a network file (plaintext)
|
|
||||||
Widget getNetworkWidget() {
|
|
||||||
// For network files we try to utilize server-side thumbnailing as much as possible.
|
|
||||||
// Thus, we do the following logic:
|
|
||||||
// - try to display our URL
|
|
||||||
// - on failure: Attempt to display the in-event thumbnail instead
|
|
||||||
// - on failrue / non-existance: Display button to download or view in-app
|
|
||||||
final mimetype = getMimetype(_requestedThumbnailOnFailure);
|
|
||||||
if (displayUrl == attachmentUrl &&
|
|
||||||
_contentRenderers.containsKey(mimetype)) {
|
|
||||||
return _contentRenderers[mimetype]!.network!(displayUrl);
|
|
||||||
} else {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
// as we change the url on-error we need a key so that the widget actually updates
|
|
||||||
key: ValueKey(displayUrl),
|
|
||||||
imageUrl: displayUrl!,
|
|
||||||
placeholder: (context, url) {
|
|
||||||
if (!widget.thumbnailOnly &&
|
|
||||||
displayUrl != thumbnailUrl &&
|
|
||||||
displayUrl == attachmentUrl) {
|
|
||||||
// we have to display the thumbnail while loading
|
|
||||||
return FutureBuilder<bool>(
|
|
||||||
future: (() async {
|
|
||||||
return await DefaultCacheManager()
|
|
||||||
.getFileFromCache(thumbnailUrl!) !=
|
|
||||||
null;
|
|
||||||
})(),
|
|
||||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return getPlaceholderWidget();
|
|
||||||
}
|
|
||||||
final effectiveUrl = snapshot.data == true
|
|
||||||
? thumbnailUrl!
|
|
||||||
: thumbnailUrlNoAnimated!;
|
|
||||||
return CachedNetworkImage(
|
|
||||||
key: ValueKey(effectiveUrl),
|
|
||||||
imageUrl: effectiveUrl,
|
|
||||||
placeholder: (c, u) => getPlaceholderWidget(),
|
|
||||||
imageBuilder: (context, imageProvider) => Image(
|
|
||||||
image: imageProvider,
|
|
||||||
frameBuilder: frameBuilder,
|
|
||||||
fit: widget.fit,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return getPlaceholderWidget();
|
|
||||||
},
|
|
||||||
imageBuilder: (context, imageProvider) => Image(
|
|
||||||
image: imageProvider,
|
|
||||||
frameBuilder: frameBuilder,
|
|
||||||
fit: widget.fit,
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) {
|
|
||||||
// the image failed to load but the event has a thumbnail attached....so we can
|
|
||||||
// try to load this one!
|
|
||||||
_requestedThumbnailOnFailure = true;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
setState(() {
|
|
||||||
thumbnailUrl = widget.event
|
|
||||||
.getAttachmentUrl(
|
|
||||||
getThumbnail: true,
|
|
||||||
useThumbnailMxcUrl: true,
|
|
||||||
animated: widget.animated)
|
|
||||||
?.toString();
|
|
||||||
thumbnailUrlNoAnimated = widget.event
|
|
||||||
.getAttachmentUrl(
|
|
||||||
getThumbnail: true,
|
|
||||||
useThumbnailMxcUrl: true,
|
|
||||||
animated: false)
|
|
||||||
?.toString();
|
|
||||||
attachmentUrl = widget.event
|
|
||||||
.getAttachmentUrl(
|
|
||||||
useThumbnailMxcUrl: true, animated: widget.animated)
|
|
||||||
?.toString();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return getPlaceholderWidget();
|
|
||||||
} else if (widget.thumbnailOnly &&
|
|
||||||
displayUrl != attachmentUrl &&
|
|
||||||
_knownMimetypes.contains(mimetype)) {
|
|
||||||
// Okay, the thumbnail failed to load, but we do know how to render the image
|
|
||||||
// ourselves. Let's offer the user a button to view it.
|
|
||||||
return getPlaceholderWidget(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
OutlinedButton(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
primary: Theme.of(context).textTheme.bodyText1!.color,
|
|
||||||
),
|
|
||||||
onPressed: () => onTap(context),
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context)!.tapToShowImage,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.event.sizeString != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(widget.event.sizeString!),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return getErrorWidget(context, error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (!tapToView) return;
|
||||||
|
showDialog(
|
||||||
|
context: Matrix.of(context).navigatorContext,
|
||||||
|
useRootNavigator: false,
|
||||||
|
builder: (_) => ImageViewer(event),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget content;
|
|
||||||
String key;
|
|
||||||
if (_error != null) {
|
|
||||||
content = getErrorWidget(context);
|
|
||||||
key = 'error';
|
|
||||||
} else if (_displayFile != null) {
|
|
||||||
content = getMemoryWidget();
|
|
||||||
key = 'memory-' + (content.key as ValueKey).value;
|
|
||||||
} else if (displayUrl != null) {
|
|
||||||
content = getNetworkWidget();
|
|
||||||
key = 'network-' + (content.key as ValueKey).value;
|
|
||||||
} else {
|
|
||||||
content = getPlaceholderWidget();
|
|
||||||
key = 'placeholder';
|
|
||||||
}
|
|
||||||
if (widget.maxSize) {
|
|
||||||
content = AspectRatio(
|
|
||||||
aspectRatio: widget.width / widget.height,
|
|
||||||
child: content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onTap(context),
|
onTap: () => _onTap(context),
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: widget.event.eventId,
|
tag: event.eventId,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 1000),
|
duration: const Duration(milliseconds: 1000),
|
||||||
child: Container(
|
child: Container(
|
||||||
key: ValueKey(key),
|
constraints: maxSize
|
||||||
constraints: widget.maxSize
|
? BoxConstraints(
|
||||||
? BoxConstraints.loose(Size(
|
maxWidth: width,
|
||||||
widget.width,
|
maxHeight: height,
|
||||||
widget.height,
|
)
|
||||||
))
|
|
||||||
: null,
|
: null,
|
||||||
child: content,
|
child: MxcImage(
|
||||||
|
event: event,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
animated: animated,
|
||||||
|
isThumbnail: thumbnailOnly,
|
||||||
|
placeholder: _buildPlaceholder,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTap(BuildContext context) {
|
|
||||||
if (widget.onTap != null) {
|
|
||||||
widget.onTap!();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!widget.tapToView) return;
|
|
||||||
showDialog(
|
|
||||||
context: Matrix.of(context).navigatorContext,
|
|
||||||
useRootNavigator: false,
|
|
||||||
builder: (_) => ImageViewer(widget.event, onLoaded: () {
|
|
||||||
// If the original file didn't load yet, we want to do that now.
|
|
||||||
// This is so that the original file displays after going on the image viewer,
|
|
||||||
// waiting for it to load, and then hitting back. This ensures that we always
|
|
||||||
// display the best image available, with requiring as little network as possible
|
|
||||||
if (_file == null) {
|
|
||||||
widget.event.isAttachmentCached().then((cached) {
|
|
||||||
if (cached) {
|
|
||||||
_requestFile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ImageBubbleContentRenderer {
|
|
||||||
final Widget Function(Uint8List, String)? memory;
|
|
||||||
final Widget Function(String?)? network;
|
|
||||||
|
|
||||||
_ImageBubbleContentRenderer({this.memory, this.network});
|
|
||||||
}
|
}
|
||||||
|
@ -100,10 +100,11 @@ class Message extends StatelessWidget {
|
|||||||
bottomRight: const Radius.circular(AppConfig.borderRadius),
|
bottomRight: const Radius.circular(AppConfig.borderRadius),
|
||||||
);
|
);
|
||||||
final noBubble = {
|
final noBubble = {
|
||||||
MessageTypes.Video,
|
MessageTypes.Video,
|
||||||
MessageTypes.Image,
|
MessageTypes.Image,
|
||||||
MessageTypes.Sticker,
|
MessageTypes.Sticker,
|
||||||
}.contains(event.messageType);
|
}.contains(event.messageType) &&
|
||||||
|
!event.redacted;
|
||||||
|
|
||||||
if (ownMessage) {
|
if (ownMessage) {
|
||||||
color = displayEvent.status.isError
|
color = displayEvent.status.isError
|
||||||
|
@ -78,6 +78,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
case MessageTypes.Sticker:
|
case MessageTypes.Sticker:
|
||||||
|
if (event.redacted) continue textmessage;
|
||||||
return Sticker(event);
|
return Sticker(event);
|
||||||
case MessageTypes.Audio:
|
case MessageTypes.Audio:
|
||||||
if (PlatformInfos.isMobile || PlatformInfos.isMacOS) {
|
if (PlatformInfos.isMobile || PlatformInfos.isMacOS) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart' show IterableExtension;
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
@ -10,6 +9,7 @@ import 'package:fluffychat/config/app_config.dart';
|
|||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
|
|
||||||
class MessageReactions extends StatelessWidget {
|
class MessageReactions extends StatelessWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
@ -112,17 +112,12 @@ class _Reaction extends StatelessWidget {
|
|||||||
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
||||||
Widget content;
|
Widget content;
|
||||||
if (reactionKey!.startsWith('mxc://')) {
|
if (reactionKey!.startsWith('mxc://')) {
|
||||||
final src = Uri.parse(reactionKey!).getThumbnail(
|
|
||||||
Matrix.of(context).client,
|
|
||||||
width: 9999,
|
|
||||||
height: fontSize! * MediaQuery.of(context).devicePixelRatio,
|
|
||||||
method: ThumbnailMethod.scale,
|
|
||||||
);
|
|
||||||
content = Row(
|
content = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CachedNetworkImage(
|
MxcImage(
|
||||||
imageUrl: src.toString(),
|
uri: Uri.parse(reactionKey!),
|
||||||
|
width: 9999,
|
||||||
height: fontSize,
|
height: fontSize,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
@ -10,6 +9,7 @@ import 'package:slugify/slugify.dart';
|
|||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../widgets/avatar.dart';
|
import '../../widgets/avatar.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
import 'command_hints.dart';
|
import 'command_hints.dart';
|
||||||
@ -251,21 +251,15 @@ class InputBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (suggestion['type'] == 'emote') {
|
if (suggestion['type'] == 'emote') {
|
||||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
final url = Uri.parse(suggestion['mxc'] ?? '').getThumbnail(
|
|
||||||
room.client,
|
|
||||||
width: size * ratio,
|
|
||||||
height: size * ratio,
|
|
||||||
method: ThumbnailMethod.scale,
|
|
||||||
animated: true,
|
|
||||||
);
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CachedNetworkImage(
|
MxcImage(
|
||||||
imageUrl: url.toString(),
|
uri: suggestion['mxc'] is String
|
||||||
|
? Uri.parse(suggestion['mxc'] ?? '')
|
||||||
|
: null,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
@ -35,8 +34,8 @@ class SsoButton extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(4.0),
|
padding: const EdgeInsets.all(4.0),
|
||||||
child: identityProvider.icon == null
|
child: identityProvider.icon == null
|
||||||
? const Icon(Icons.web_outlined)
|
? const Icon(Icons.web_outlined)
|
||||||
: CachedNetworkImage(
|
: Image.network(
|
||||||
imageUrl: Uri.parse(identityProvider.icon!)
|
Uri.parse(identityProvider.icon!)
|
||||||
.getDownloadLink(
|
.getDownloadLink(
|
||||||
Matrix.of(context).getLoginClient())
|
Matrix.of(context).getLoginClient())
|
||||||
.toString(),
|
.toString(),
|
||||||
|
@ -10,9 +10,8 @@ import '../../utils/matrix_sdk_extensions.dart/event_extension.dart';
|
|||||||
|
|
||||||
class ImageViewer extends StatefulWidget {
|
class ImageViewer extends StatefulWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
final void Function()? onLoaded;
|
|
||||||
|
|
||||||
const ImageViewer(this.event, {Key? key, this.onLoaded}) : super(key: key);
|
const ImageViewer(this.event, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageViewerController createState() => ImageViewerController();
|
ImageViewerController createState() => ImageViewerController();
|
||||||
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import 'image_viewer.dart';
|
import 'image_viewer.dart';
|
||||||
|
|
||||||
class ImageViewerView extends StatelessWidget {
|
class ImageViewerView extends StatelessWidget {
|
||||||
@ -53,15 +53,14 @@ class ImageViewerView extends StatelessWidget {
|
|||||||
maxScale: 10.0,
|
maxScale: 10.0,
|
||||||
onInteractionEnd: controller.onInteractionEnds,
|
onInteractionEnd: controller.onInteractionEnds,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ImageBubble(
|
child: Hero(
|
||||||
controller.widget.event,
|
tag: controller.widget.event.eventId,
|
||||||
tapToView: false,
|
child: MxcImage(
|
||||||
onLoaded: controller.widget.onLoaded,
|
event: controller.widget.event,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
backgroundColor: Colors.black,
|
isThumbnail: false,
|
||||||
maxSize: false,
|
animated: true,
|
||||||
thumbnailOnly: false,
|
),
|
||||||
animated: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
import 'settings_emotes.dart';
|
import 'settings_emotes.dart';
|
||||||
|
|
||||||
@ -216,15 +216,8 @@ class _EmoteImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const size = 38.0;
|
const size = 38.0;
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
return MxcImage(
|
||||||
final url = mxc.getThumbnail(
|
uri: mxc,
|
||||||
Matrix.of(context).client,
|
|
||||||
width: size * devicePixelRatio,
|
|
||||||
height: size * devicePixelRatio,
|
|
||||||
method: ThumbnailMethod.scale,
|
|
||||||
);
|
|
||||||
return CachedNetworkImage(
|
|
||||||
imageUrl: url.toString(),
|
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/string_color.dart';
|
import 'package:fluffychat/utils/string_color.dart';
|
||||||
import 'matrix.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
|
|
||||||
class Avatar extends StatelessWidget {
|
class Avatar extends StatelessWidget {
|
||||||
final Uri? mxContent;
|
final Uri? mxContent;
|
||||||
@ -27,11 +26,6 @@ class Avatar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final src = mxContent?.getThumbnail(
|
|
||||||
client ?? Matrix.of(context).client,
|
|
||||||
width: size * MediaQuery.of(context).devicePixelRatio,
|
|
||||||
height: size * MediaQuery.of(context).devicePixelRatio,
|
|
||||||
);
|
|
||||||
var fallbackLetters = '@';
|
var fallbackLetters = '@';
|
||||||
final name = this.name;
|
final name = this.name;
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
@ -68,17 +62,12 @@ class Avatar extends StatelessWidget {
|
|||||||
noPic ? name?.lightColor : Theme.of(context).secondaryHeaderColor,
|
noPic ? name?.lightColor : Theme.of(context).secondaryHeaderColor,
|
||||||
child: noPic
|
child: noPic
|
||||||
? textWidget
|
? textWidget
|
||||||
: CachedNetworkImage(
|
: MxcImage(
|
||||||
imageUrl: src.toString(),
|
uri: mxContent,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
placeholder: (c, s) => textWidget,
|
placeholder: (_) => textWidget,
|
||||||
errorWidget: (c, s, d) => Stack(
|
|
||||||
children: [
|
|
||||||
textWidget,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'matrix.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
|
|
||||||
class ContentBanner extends StatelessWidget {
|
class ContentBanner extends StatelessWidget {
|
||||||
final Uri? mxContent;
|
final Uri? mxContent;
|
||||||
final double height;
|
final double height;
|
||||||
final IconData defaultIcon;
|
final IconData defaultIcon;
|
||||||
final bool loading;
|
|
||||||
final void Function()? onEdit;
|
final void Function()? onEdit;
|
||||||
final Client? client;
|
final Client? client;
|
||||||
final double opacity;
|
final double opacity;
|
||||||
@ -21,7 +17,6 @@ class ContentBanner extends StatelessWidget {
|
|||||||
{this.mxContent,
|
{this.mxContent,
|
||||||
this.height = 400,
|
this.height = 400,
|
||||||
this.defaultIcon = Icons.account_circle_outlined,
|
this.defaultIcon = Icons.account_circle_outlined,
|
||||||
this.loading = false,
|
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
this.client,
|
this.client,
|
||||||
this.opacity = 0.75,
|
this.opacity = 0.75,
|
||||||
@ -47,44 +42,27 @@ class ContentBanner extends StatelessWidget {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: opacity,
|
opacity: opacity,
|
||||||
child: (!loading)
|
child: LayoutBuilder(
|
||||||
? LayoutBuilder(builder:
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
(BuildContext context, BoxConstraints constraints) {
|
return Hero(
|
||||||
// #775 don't request new image resolution on every resize
|
tag: heroTag,
|
||||||
// by rounding up to the next multiple of stepSize
|
child: MxcImage(
|
||||||
const stepSize = 300;
|
uri: mxContent,
|
||||||
final bannerSize =
|
animated: true,
|
||||||
constraints.maxWidth * window.devicePixelRatio;
|
fit: BoxFit.cover,
|
||||||
final steppedBannerSize =
|
height: 400,
|
||||||
(bannerSize / stepSize).ceil() * stepSize;
|
width: 800,
|
||||||
final src = mxContent?.getThumbnail(
|
placeholder: (c) => Center(
|
||||||
client ?? Matrix.of(context).client,
|
child: Icon(
|
||||||
width: steppedBannerSize,
|
defaultIcon,
|
||||||
height: steppedBannerSize,
|
size: 200,
|
||||||
method: ThumbnailMethod.scale,
|
color:
|
||||||
animated: true,
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
);
|
),
|
||||||
return Hero(
|
|
||||||
tag: heroTag,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: src.toString(),
|
|
||||||
height: 300,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorWidget: (c, m, e) => Icon(
|
|
||||||
defaultIcon,
|
|
||||||
size: 200,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: Icon(
|
|
||||||
defaultIcon,
|
|
||||||
size: 200,
|
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onEdit != null)
|
if (onEdit != null)
|
||||||
|
147
lib/widgets/mxc_image.dart
Normal file
147
lib/widgets/mxc_image.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
class MxcImage extends StatefulWidget {
|
||||||
|
final Uri? uri;
|
||||||
|
final Event? event;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final bool isThumbnail;
|
||||||
|
final bool animated;
|
||||||
|
final Duration retryDuration;
|
||||||
|
final Duration animationDuration;
|
||||||
|
final Curve animationCurve;
|
||||||
|
final ThumbnailMethod thumbnailMethod;
|
||||||
|
final Widget Function(BuildContext context)? placeholder;
|
||||||
|
|
||||||
|
const MxcImage({
|
||||||
|
this.uri,
|
||||||
|
this.event,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fit,
|
||||||
|
this.placeholder,
|
||||||
|
this.isThumbnail = true,
|
||||||
|
this.animated = false,
|
||||||
|
this.animationDuration = const Duration(milliseconds: 200),
|
||||||
|
this.retryDuration = const Duration(seconds: 2),
|
||||||
|
this.animationCurve = Curves.linear,
|
||||||
|
this.thumbnailMethod = ThumbnailMethod.scale,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MxcImage> createState() => _MxcImageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MxcImageState extends State<MxcImage> {
|
||||||
|
Uint8List? _imageData;
|
||||||
|
bool? _isCached;
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final client = Matrix.of(context).client;
|
||||||
|
final uri = widget.uri;
|
||||||
|
final event = widget.event;
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
final width = widget.width;
|
||||||
|
final realWidth = width == null ? null : width * devicePixelRatio;
|
||||||
|
final height = widget.height;
|
||||||
|
final realHeight = height == null ? null : height * devicePixelRatio;
|
||||||
|
|
||||||
|
final httpUri = widget.isThumbnail
|
||||||
|
? uri.getThumbnail(
|
||||||
|
client,
|
||||||
|
width: realWidth,
|
||||||
|
height: realHeight,
|
||||||
|
animated: widget.animated,
|
||||||
|
method: widget.thumbnailMethod,
|
||||||
|
)
|
||||||
|
: uri.getDownloadLink(client);
|
||||||
|
|
||||||
|
final storeKey = widget.isThumbnail ? httpUri : uri;
|
||||||
|
|
||||||
|
if (_isCached == null) {
|
||||||
|
final cachedData = await client.database?.getFile(storeKey);
|
||||||
|
if (cachedData != null) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_imageData = cachedData;
|
||||||
|
_isCached = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isCached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteData = await http.get(httpUri).then((r) => r.bodyBytes);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_imageData = remoteData;
|
||||||
|
});
|
||||||
|
await client.database?.storeFile(storeKey, remoteData, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event != null) {
|
||||||
|
final data = await event.downloadAndDecryptAttachment(
|
||||||
|
getThumbnail: widget.isThumbnail,
|
||||||
|
);
|
||||||
|
if (data.detectFileType is MatrixImageFile) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_imageData = data.bytes;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryLoad(_) async {
|
||||||
|
try {
|
||||||
|
await _load();
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
await Future.delayed(widget.retryDuration);
|
||||||
|
_tryLoad(_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(_tryLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final data = _imageData;
|
||||||
|
|
||||||
|
return AnimatedCrossFade(
|
||||||
|
duration: widget.animationDuration,
|
||||||
|
crossFadeState:
|
||||||
|
data == null ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
|
firstChild: widget.placeholder?.call(context) ??
|
||||||
|
const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
secondChild: data == null
|
||||||
|
? Container()
|
||||||
|
: Image.memory(
|
||||||
|
data,
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
fit: widget.fit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -107,7 +107,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
@ -12,7 +12,6 @@ dependencies:
|
|||||||
animations: ^2.0.2
|
animations: ^2.0.2
|
||||||
async: ^2.8.2
|
async: ^2.8.2
|
||||||
blurhash_dart: ^1.1.0
|
blurhash_dart: ^1.1.0
|
||||||
cached_network_image: ^3.2.0
|
|
||||||
callkeep: ^0.3.2
|
callkeep: ^0.3.2
|
||||||
chewie: ^1.2.2
|
chewie: ^1.2.2
|
||||||
collection: ^1.15.0-nullsafety.4
|
collection: ^1.15.0-nullsafety.4
|
||||||
|
Loading…
Reference in New Issue
Block a user