Fix bug && Provide a way to handle the image errors and use custom image provider

pull/1428/head
Ahmed Hnewa 2 years ago
parent 5d30113804
commit ed1975a3de
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 99
      flutter_quill_extensions/lib/embeds/builders.dart
  2. 10
      flutter_quill_extensions/lib/embeds/embed_types.dart
  3. 118
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  4. 51
      flutter_quill_extensions/lib/embeds/toolbar/image_button.dart
  5. 28
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  6. 9
      flutter_quill_extensions/lib/embeds/toolbar/media_button.dart
  7. 62
      flutter_quill_extensions/lib/embeds/widgets/image.dart
  8. 23
      flutter_quill_extensions/lib/flutter_quill_extensions.dart

@ -21,13 +21,17 @@ import 'widgets/youtube_video_app.dart';
class ImageEmbedBuilder extends EmbedBuilder { class ImageEmbedBuilder extends EmbedBuilder {
ImageEmbedBuilder({ ImageEmbedBuilder({
this.onImageRemovedCallback, required this.imageProviderBuilder,
this.shouldRemoveImageCallback, required this.imageErrorWidgetBuilder,
required this.onImageRemovedCallback,
required this.shouldRemoveImageCallback,
this.forceUseMobileOptionMenu = false, this.forceUseMobileOptionMenu = false,
}); });
final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback; final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback;
final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback; final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback;
final bool forceUseMobileOptionMenu; final bool forceUseMobileOptionMenu;
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
@override @override
String get key => BlockEmbed.imageType; String get key => BlockEmbed.imageType;
@ -71,12 +75,23 @@ class ImageEmbedBuilder extends EmbedBuilder {
final a = base.getAlignment(attrs[Attribute.mobileAlignment]); final a = base.getAlignment(attrs[Attribute.mobileAlignment]);
image = Padding( image = Padding(
padding: EdgeInsets.all(m), padding: EdgeInsets.all(m),
child: imageByUrl(imageUrl, width: w, height: h, alignment: a)); child: imageByUrl(
imageUrl,
width: w,
height: h,
alignment: a,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
));
} }
} }
if (imageSize == null) { if (imageSize == null) {
image = imageByUrl(imageUrl); image = imageByUrl(
imageUrl,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
);
imageSize = OptionalSize((image as Image).width, image.height); imageSize = OptionalSize((image as Image).width, image.height);
} }
@ -97,20 +112,21 @@ class ImageEmbedBuilder extends EmbedBuilder {
builder: (context) { builder: (context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
return ImageResizer( return ImageResizer(
onImageResize: (w, h) { onImageResize: (w, h) {
final res = getEmbedNode( final res = getEmbedNode(
controller, controller.selection.start); controller, controller.selection.start);
final attr = base.replaceStyleString( final attr = base.replaceStyleString(
getImageStyleString(controller), w, h); getImageStyleString(controller), w, h);
controller controller
..skipRequestKeyboard = true ..skipRequestKeyboard = true
..formatText( ..formatText(
res.offset, 1, StyleAttribute(attr)); res.offset, 1, StyleAttribute(attr));
}, },
imageWidth: imageSize?.width, imageWidth: imageSize?.width,
imageHeight: imageSize?.height, imageHeight: imageSize?.height,
maxWidth: screenSize.width, maxWidth: screenSize.width,
maxHeight: screenSize.height); maxHeight: screenSize.height,
);
}); });
}, },
); );
@ -161,7 +177,10 @@ class ImageEmbedBuilder extends EmbedBuilder {
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog( child: SimpleDialog(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
children: [ children: [
if (base.isMobile()) resizeOption, if (base.isMobile()) resizeOption,
copyOption, copyOption,
@ -179,9 +198,11 @@ class ImageEmbedBuilder extends EmbedBuilder {
// and that is up to the developer // and that is up to the developer
if (!base.isMobile() && forceUseMobileOptionMenu) { if (!base.isMobile() && forceUseMobileOptionMenu) {
return _menuOptionsForReadonlyImage( return _menuOptionsForReadonlyImage(
context, context: context,
imageUrl, imageUrl: imageUrl,
image, image: image,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
); );
} }
return image; return image;
@ -189,9 +210,11 @@ class ImageEmbedBuilder extends EmbedBuilder {
// We provide option menu for mobile platform excluding base64 image // We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage( return _menuOptionsForReadonlyImage(
context, context: context,
imageUrl, imageUrl: imageUrl,
image, image: image,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
); );
} }
} }
@ -299,8 +322,13 @@ class FormulaEmbedBuilder extends EmbedBuilder {
} }
} }
Widget _menuOptionsForReadonlyImage( Widget _menuOptionsForReadonlyImage({
BuildContext context, String imageUrl, Widget image) { required BuildContext context,
required String imageUrl,
required Widget image,
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
}) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
@ -351,10 +379,19 @@ Widget _menuOptionsForReadonlyImage(
text: 'Zoom'.i18n, text: 'Zoom'.i18n,
onPressed: () { onPressed: () {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( // TODO: Consider add support for other theme system
builder: (context) => // like Cupertino or at least add the option to by
ImageTapWrapper(imageUrl: imageUrl))); // by using PageRoute as option so dev can ovveride this
// this change should be done in all places if you want to
MaterialPageRoute(
builder: (context) => ImageTapWrapper(
imageUrl: imageUrl,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
),
),
);
}, },
); );
return Padding( return Padding(

@ -1,7 +1,8 @@
import 'dart:io' show File; import 'dart:io' show File;
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart'
show ImageErrorWidgetBuilder, BuildContext, ImageProvider;
typedef OnImagePickCallback = Future<String?> Function(File file); typedef OnImagePickCallback = Future<String?> Function(File file);
typedef OnVideoPickCallback = Future<String?> Function(File file); typedef OnVideoPickCallback = Future<String?> Function(File file);
@ -52,3 +53,10 @@ typedef ImageEmbedBuilderWillRemoveCallback = Future<bool> Function(
typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function( typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function(
File imageFile, File imageFile,
); );
typedef ImageEmbedBuilderProviderBuilder = ImageProvider Function(
String imageUrl,
// {required bool isLocalImage}
);
typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder;

@ -61,30 +61,49 @@ class CameraButton extends StatelessWidget {
size: iconSize * 1.77, size: iconSize * 1.77,
fillColor: iconFillColor, fillColor: iconFillColor,
borderRadius: iconTheme?.borderRadius ?? 2, borderRadius: iconTheme?.borderRadius ?? 2,
onPressed: () => _handleCameraButtonTap(context, controller, onPressed: () => _handleCameraButtonTap(
onImagePickCallback: onImagePickCallback, context,
onVideoPickCallback: onVideoPickCallback, controller,
filePickImpl: filePickImpl, onImagePickCallback: onImagePickCallback,
webImagePickImpl: webImagePickImpl), onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
),
); );
} }
Future<void> _handleCameraButtonTap( Future<void> _handleCameraButtonTap(
BuildContext context, QuillController controller, BuildContext context,
{OnImagePickCallback? onImagePickCallback, QuillController controller, {
OnVideoPickCallback? onVideoPickCallback, OnImagePickCallback? onImagePickCallback,
FilePickImpl? filePickImpl, OnVideoPickCallback? onVideoPickCallback,
WebImagePickImpl? webImagePickImpl}) async { FilePickImpl? filePickImpl,
if (onImagePickCallback != null && onVideoPickCallback != null) { WebImagePickImpl? webImagePickImpl,
final selector = cameraPickSettingSelector ?? }) async {
(context) => showDialog<MediaPickSetting>( if (onVideoPickCallback == null && onImagePickCallback == null) {
context: context, throw ArgumentError(
builder: (ctx) => AlertDialog( 'onImagePickCallback and onVideoPickCallback are both null',
contentPadding: EdgeInsets.zero, );
backgroundColor: Colors.transparent, }
content: Column(
mainAxisSize: MainAxisSize.min, var shouldShowPickPhotoByCamera = false;
children: [ var shouldShowRecordVideoByCamera = false;
if (onImagePickCallback != null) {
shouldShowPickPhotoByCamera = true;
}
if (onVideoPickCallback != null) {
shouldShowRecordVideoByCamera = true;
}
final selector = cameraPickSettingSelector ??
(context) => showDialog<MediaPickSetting>(
context: context,
builder: (ctx) => AlertDialog(
contentPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (shouldShowPickPhotoByCamera)
TextButton.icon( TextButton.icon(
icon: const Icon( icon: const Icon(
Icons.camera, Icons.camera,
@ -94,6 +113,7 @@ class CameraButton extends StatelessWidget {
onPressed: () => onPressed: () =>
Navigator.pop(ctx, MediaPickSetting.Camera), Navigator.pop(ctx, MediaPickSetting.Camera),
), ),
if (shouldShowRecordVideoByCamera)
TextButton.icon( TextButton.icon(
icon: const Icon( icon: const Icon(
Icons.video_call, Icons.video_call,
@ -103,28 +123,44 @@ class CameraButton extends StatelessWidget {
onPressed: () => onPressed: () =>
Navigator.pop(ctx, MediaPickSetting.Video), Navigator.pop(ctx, MediaPickSetting.Video),
) )
], ],
),
), ),
); ),
);
final source = await selector(context);
if (source != null) { final source = await selector(context);
switch (source) { if (source == null) {
case MediaPickSetting.Camera: return;
await ImageVideoUtils.handleImageButtonTap( }
context, controller, ImageSource.camera, onImagePickCallback, switch (source) {
filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl); case MediaPickSetting.Camera:
break; await ImageVideoUtils.handleImageButtonTap(
case MediaPickSetting.Video: context,
await ImageVideoUtils.handleVideoButtonTap( controller,
context, controller, ImageSource.camera, onVideoPickCallback, ImageSource.camera,
filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl); onImagePickCallback!,
break; filePickImpl: filePickImpl,
default: webImagePickImpl: webImagePickImpl,
throw ArgumentError('Invalid MediaSetting'); );
} break;
} case MediaPickSetting.Video:
await ImageVideoUtils.handleVideoButtonTap(
context,
controller,
ImageSource.camera,
onVideoPickCallback!,
filePickImpl: filePickImpl,
webVideoPickImpl: webVideoPickImpl,
);
break;
case MediaPickSetting.Gallery:
throw ArgumentError(
'Invalid MediaSetting for the camera button',
);
case MediaPickSetting.Link:
throw ArgumentError(
'Invalid MediaSetting for the camera button',
);
} }
} }
} }

@ -64,20 +64,47 @@ class ImageButton extends StatelessWidget {
} }
Future<void> _onPressedHandler(BuildContext context) async { Future<void> _onPressedHandler(BuildContext context) async {
if (onImagePickCallback != null) { final onImagePickCallbackRef = onImagePickCallback;
final selector = if (onImagePickCallbackRef == null) {
mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
final source = await selector(context);
if (source != null) {
if (source == MediaPickSetting.Gallery) {
_pickImage(context);
} else {
_typeLink(context);
}
}
} else {
_typeLink(context); _typeLink(context);
return;
} }
final selector =
mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
final source = await selector(context);
if (source == null) {
return;
}
switch (source) {
case MediaPickSetting.Gallery:
_pickImage(context);
break;
case MediaPickSetting.Link:
_typeLink(context);
break;
case MediaPickSetting.Camera:
await ImageVideoUtils.handleImageButtonTap(
context,
controller,
ImageSource.camera,
onImagePickCallbackRef,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
);
break;
case MediaPickSetting.Video:
throw ArgumentError(
'Sorry but this is the Image button and not the video one',
);
}
// This will not work for the pick image using camera (bug fix)
// if (source != null) {
// if (source == MediaPickSetting.Gallery) {
// } else {
// _typeLink(context);
// }
} }
void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(

@ -38,6 +38,10 @@ class LinkDialogState extends State<LinkDialog> {
// TODO: Consider replace the default Regex with this one // TODO: Consider replace the default Regex with this one
// Since that is not the reason I sent the changes then I will not edit it // Since that is not the reason I sent the changes then I will not edit it
// TODO: Consider use one of those as default or provide a
// way to custmize the check, that are not based on RegExp,
// I already implemented one so tell me if you are interested
// final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure // final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure
// final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure // final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure
// _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp; // _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp;
@ -53,9 +57,10 @@ class LinkDialogState extends State<LinkDialog> {
maxLines: null, maxLines: null,
style: widget.dialogTheme?.inputTextStyle, style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Paste a link'.i18n, labelText: 'Paste a link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true, autofocus: true,
onChanged: _linkChanged, onChanged: _linkChanged,
controller: _controller, controller: _controller,
@ -119,12 +124,13 @@ class ImageVideoUtils {
/// For image picking logic /// For image picking logic
static Future<void> handleImageButtonTap( static Future<void> handleImageButtonTap(
BuildContext context, BuildContext context,
QuillController controller, QuillController controller,
ImageSource imageSource, ImageSource imageSource,
OnImagePickCallback onImagePickCallback, OnImagePickCallback onImagePickCallback, {
{FilePickImpl? filePickImpl, FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl}) async { WebImagePickImpl? webImagePickImpl,
}) async {
final index = controller.selection.baseOffset; final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index; final length = controller.selection.extentOffset - index;
@ -149,7 +155,9 @@ class ImageVideoUtils {
} }
static Future<String?> _pickImage( static Future<String?> _pickImage(
ImageSource source, OnImagePickCallback onImagePickCallback) async { ImageSource source,
OnImagePickCallback onImagePickCallback,
) async {
final pickedFile = await ImagePicker().pickImage(source: source); final pickedFile = await ImagePicker().pickImage(source: source);
if (pickedFile == null) { if (pickedFile == null) {
return null; return null;

@ -118,7 +118,8 @@ class MediaButton extends StatelessWidget {
Future<void> _pickImage() async { Future<void> _pickImage() async {
if (!(kIsWeb || isMobile() || isDesktop())) { if (!(kIsWeb || isMobile() || isDesktop())) {
throw UnsupportedError( throw UnsupportedError(
'Unsupported target platform: ${defaultTargetPlatform.name}'); 'Unsupported target platform: ${defaultTargetPlatform.name}',
);
} }
final mediaFileUrl = await _pickMediaFileUrl(); final mediaFileUrl = await _pickMediaFileUrl();
@ -127,7 +128,11 @@ class MediaButton extends StatelessWidget {
final index = controller.selection.baseOffset; final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index; final length = controller.selection.extentOffset - index;
controller.replaceText( controller.replaceText(
index, length, BlockEmbed.image(mediaFileUrl), null); index,
length,
BlockEmbed.image(mediaFileUrl),
null,
);
} }
} }

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import '../embed_types.dart';
import '../utils.dart'; import '../utils.dart';
const List<String> imageFileExtensions = [ const List<String> imageFileExtensions = [
@ -27,21 +28,44 @@ String getImageStyleString(QuillController controller) {
return s ?? ''; return s ?? '';
} }
Image imageByUrl(String imageUrl, Image imageByUrl(
{double? width, String imageUrl, {
double? height, required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
AlignmentGeometry alignment = Alignment.center}) { required ImageErrorWidgetBuilder? imageErrorWidgetBuilder,
double? width,
double? height,
AlignmentGeometry alignment = Alignment.center,
}) {
if (isImageBase64(imageUrl)) { if (isImageBase64(imageUrl)) {
return Image.memory(base64.decode(imageUrl), return Image.memory(base64.decode(imageUrl),
width: width, height: height, alignment: alignment); width: width, height: height, alignment: alignment);
} }
if (imageUrl.startsWith('http')) { if (imageProviderBuilder != null) {
return Image.network(imageUrl, return Image(
width: width, height: height, alignment: alignment); image: imageProviderBuilder(imageUrl),
width: width,
height: height,
alignment: alignment,
errorBuilder: imageErrorWidgetBuilder,
);
}
if (isHttpBasedUrl(imageUrl)) {
return Image.network(
imageUrl,
width: width,
height: height,
alignment: alignment,
errorBuilder: imageErrorWidgetBuilder,
);
} }
return Image.file(File(imageUrl), return Image.file(
width: width, height: height, alignment: alignment); File(imageUrl),
width: width,
height: height,
alignment: alignment,
errorBuilder: imageErrorWidgetBuilder,
);
} }
String standardizeImageUrl(String url) { String standardizeImageUrl(String url) {
@ -73,12 +97,22 @@ String appendFileExtensionToImageUrl(String url) {
class ImageTapWrapper extends StatelessWidget { class ImageTapWrapper extends StatelessWidget {
const ImageTapWrapper({ const ImageTapWrapper({
required this.imageUrl, required this.imageUrl,
required this.imageProviderBuilder,
required this.imageErrorWidgetBuilder,
}); });
final String imageUrl; final String imageUrl;
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
ImageProvider _imageProviderByUrl(String imageUrl) { ImageProvider _imageProviderByUrl(
if (imageUrl.startsWith('http')) { String imageUrl, {
required ImageEmbedBuilderProviderBuilder? customImageProviderBuilder,
}) {
if (customImageProviderBuilder != null) {
return customImageProviderBuilder(imageUrl);
}
if (isHttpBasedUrl(imageUrl)) {
return NetworkImage(imageUrl); return NetworkImage(imageUrl);
} }
@ -95,7 +129,11 @@ class ImageTapWrapper extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
PhotoView( PhotoView(
imageProvider: _imageProviderByUrl(imageUrl), imageProvider: _imageProviderByUrl(
imageUrl,
customImageProviderBuilder: imageProviderBuilder,
),
errorBuilder: imageErrorWidgetBuilder,
loadingBuilder: (context, event) { loadingBuilder: (context, event) {
return Container( return Container(
color: Colors.black, color: Colors.black,

@ -72,11 +72,28 @@ class FlutterQuillEmbeds {
/// ), /// ),
/// ); /// );
/// ///
/// // Return `true` to allow image removal if the user confirms, otherwise `false` /// // Return `true` to allow image removal if the user confirms, otherwise
/// `false`
/// return isShouldRemove; /// return isShouldRemove;
/// } /// }
/// ``` /// ```
/// ///
/// [imageProviderBuilder] if you want to use custom image provider, please
/// pass a value to this property
/// By default we will use [NetworkImage] provider if the image url/path
/// is using http/https, if not then we will use [FileImage] provider
/// If you ovveride this make sure to handle the case where if the [imageUrl]
/// is in the local storage or it does exists in the system file
/// or use the same way we did it
///
/// Example of [imageProviderBuilder] customization:
/// ```dart
/// imageProviderBuilder: (imageUrl) async {
/// // Example of using cached_network_image package
/// return CachedNetworkImageProvider(imageUrl);
/// }
/// ```
///
/// [forceUseMobileOptionMenuForImageClick] is a boolean /// [forceUseMobileOptionMenuForImageClick] is a boolean
/// flag that, when set to `true`, /// flag that, when set to `true`,
/// enforces the use of the mobile-specific option menu for image clicks in /// enforces the use of the mobile-specific option menu for image clicks in
@ -107,10 +124,14 @@ class FlutterQuillEmbeds {
void Function(GlobalKey videoContainerKey)? onVideoInit, void Function(GlobalKey videoContainerKey)? onVideoInit,
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback, ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback,
ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
bool forceUseMobileOptionMenuForImageClick = false, bool forceUseMobileOptionMenuForImageClick = false,
}) => }) =>
[ [
ImageEmbedBuilder( ImageEmbedBuilder(
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
imageProviderBuilder: imageProviderBuilder,
forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick, forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick,
onImageRemovedCallback: onImageRemovedCallback ?? onImageRemovedCallback: onImageRemovedCallback ??
(imageFile) async { (imageFile) async {

Loading…
Cancel
Save