Bug fixes + Provide a way to handle the image errors and use custom image provider (#1428)

pull/1438/head
Ahmed Hnewa 1 year ago committed by GitHub
parent 4b87821d3f
commit db143c9556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 0
      FETCH_HEAD
  3. 4
      example/lib/pages/home_page.dart
  4. 8
      example/lib/universal_ui/universal_ui.dart
  5. 8
      example/lib/widgets/responsive_widget.dart
  6. 7
      flutter_quill_extensions/CHANGELOG.md
  7. 365
      flutter_quill_extensions/lib/embeds/builders.dart
  8. 10
      flutter_quill_extensions/lib/embeds/embed_types.dart
  9. 109
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  10. 51
      flutter_quill_extensions/lib/embeds/toolbar/image_button.dart
  11. 28
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  12. 22
      flutter_quill_extensions/lib/embeds/toolbar/media_button.dart
  13. 3
      flutter_quill_extensions/lib/embeds/utils.dart
  14. 66
      flutter_quill_extensions/lib/embeds/widgets/image.dart
  15. 11
      flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart
  16. 30
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  17. 2
      flutter_quill_extensions/pubspec.yaml
  18. 8
      lib/src/models/documents/attribute.dart
  19. 14
      lib/src/models/rules/rule.dart
  20. 7
      lib/src/translations/toolbar.i18n.dart
  21. 6
      lib/src/utils/platform.dart
  22. 30
      lib/src/utils/string.dart
  23. 2
      lib/src/widgets/editor.dart
  24. 2
      lib/src/widgets/raw_editor.dart
  25. 2
      lib/src/widgets/text_block.dart
  26. 5
      lib/src/widgets/toolbar/link_style_button2.dart
  27. 2
      pubspec.yaml

@ -1,3 +1,7 @@
# [7.4.14]
- Custom style attrbuites for platforms other than mobile (alignment, margin, width, height)
- Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)`
# [7.4.13] # [7.4.13]
- Fixed tab editing when in readOnly mode. - Fixed tab editing when in readOnly mode.

@ -104,7 +104,7 @@ class _HomePageState extends State<HomePage> {
), ),
drawer: Container( drawer: Container(
constraints: constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7), BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.7),
color: Colors.grey.shade800, color: Colors.grey.shade800,
child: _buildMenuBar(context), child: _buildMenuBar(context),
), ),
@ -408,7 +408,7 @@ class _HomePageState extends State<HomePage> {
); );
Widget _buildMenuBar(BuildContext context) { Widget _buildMenuBar(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.sizeOf(context);
const itemStyle = TextStyle( const itemStyle = TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,

@ -45,7 +45,7 @@ class ImageEmbedBuilderWeb extends EmbedBuilder {
// TODO: handle imageUrl of base64 // TODO: handle imageUrl of base64
return const SizedBox(); return const SizedBox();
} }
final size = MediaQuery.of(context).size; final size = MediaQuery.sizeOf(context);
UniversalUI().platformViewRegistry.registerViewFactory(imageUrl, (viewId) { UniversalUI().platformViewRegistry.registerViewFactory(imageUrl, (viewId) {
return html.ImageElement() return html.ImageElement()
..src = imageUrl ..src = imageUrl
@ -61,7 +61,7 @@ class ImageEmbedBuilderWeb extends EmbedBuilder {
: size.width * 0.2, : size.width * 0.2,
), ),
child: SizedBox( child: SizedBox(
height: MediaQuery.of(context).size.height * 0.45, height: MediaQuery.sizeOf(context).height * 0.45,
child: HtmlElementView( child: HtmlElementView(
viewType: imageUrl, viewType: imageUrl,
), ),
@ -94,8 +94,8 @@ class VideoEmbedBuilderWeb extends EmbedBuilder {
UniversalUI().platformViewRegistry.registerViewFactory( UniversalUI().platformViewRegistry.registerViewFactory(
videoUrl, videoUrl,
(id) => html.IFrameElement() (id) => html.IFrameElement()
..width = MediaQuery.of(context).size.width.toString() ..width = MediaQuery.sizeOf(context).width.toString()
..height = MediaQuery.of(context).size.height.toString() ..height = MediaQuery.sizeOf(context).height.toString()
..src = videoUrl ..src = videoUrl
..style.border = 'none'); ..style.border = 'none');

@ -13,16 +13,16 @@ class ResponsiveWidget extends StatelessWidget {
final Widget? smallScreen; final Widget? smallScreen;
static bool isSmallScreen(BuildContext context) { static bool isSmallScreen(BuildContext context) {
return MediaQuery.of(context).size.width < 800; return MediaQuery.sizeOf(context).width < 800;
} }
static bool isLargeScreen(BuildContext context) { static bool isLargeScreen(BuildContext context) {
return MediaQuery.of(context).size.width > 1200; return MediaQuery.sizeOf(context).width > 1200;
} }
static bool isMediumScreen(BuildContext context) { static bool isMediumScreen(BuildContext context) {
return MediaQuery.of(context).size.width >= 800 && return MediaQuery.sizeOf(context).width >= 800 &&
MediaQuery.of(context).size.width <= 1200; MediaQuery.sizeOf(context).width <= 1200;
} }
@override @override

@ -1,8 +1,15 @@
## 0.5.1 ## 0.5.1
- Provide a way to use custom image provider for the image widgets
- Provide a way to handle different errors in image widgets
- Two bug fixes related to pick the image and capture it using the camera
- Add support for image resizing on desktop platforms when forced using the mobile context menu
- Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)`
- Fix warrning "The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from dart:ui_web instead." - Fix warrning "The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from dart:ui_web instead."
- Add QuillImageUtilities class - Add QuillImageUtilities class
- Small improvemenets - Small improvemenets
- Allow to use the mobile context menu on desktop by force using it - Allow to use the mobile context menu on desktop by force using it
- Add the resizing option to the forced mobile context menu
- Add new custom style attrbuite for desktop and other platforms
## 0.5.0 ## 0.5.0
- Migrated from `gallery_saver` to `gal` for saving images - Migrated from `gallery_saver` to `gal` for saving images

@ -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;
@ -50,33 +54,73 @@ class ImageEmbedBuilder extends EmbedBuilder {
final imageUrl = standardizeImageUrl(node.value.data); final imageUrl = standardizeImageUrl(node.value.data);
OptionalSize? imageSize; OptionalSize? imageSize;
final style = node.style.attributes['style']; final style = node.style.attributes['style'];
if (base.isMobile() && style != null) {
final attrs = base.parseKeyValuePairs(style.value.toString(), { // TODO: Please use the one from [Attribute.margin]
Attribute.mobileWidth, const marginKey = 'margin';
Attribute.mobileHeight, // TODO: Please use the one from [Attribute.alignment]
Attribute.mobileMargin, const alignmentKey = 'alignment';
Attribute.mobileAlignment if (style != null) {
}); final attrs = base.isMobile()
? base.parseKeyValuePairs(style.value.toString(), {
Attribute.mobileWidth,
Attribute.mobileHeight,
Attribute.mobileMargin,
Attribute.mobileAlignment,
})
: base.parseKeyValuePairs(style.value.toString(), {
Attribute.width.key,
Attribute.height.key,
marginKey,
alignmentKey,
});
if (attrs.isNotEmpty) { if (attrs.isNotEmpty) {
final width = double.tryParse(
(base.isMobile()
? attrs[Attribute.mobileWidth]
: attrs[Attribute.width.key]) ??
'',
);
final height = double.tryParse(
(base.isMobile()
? attrs[Attribute.mobileHeight]
: attrs[Attribute.height.key]) ??
'',
);
final alignment = base.getAlignment(base.isMobile()
? attrs[Attribute.mobileAlignment]
: attrs[alignmentKey]);
final margin = (base.isMobile()
? double.tryParse(Attribute.mobileMargin)
: double.tryParse(marginKey)) ??
0.0;
assert( assert(
attrs[Attribute.mobileWidth] != null && width != null && height != null,
attrs[Attribute.mobileHeight] != null, base.isMobile()
'mobileWidth and mobileHeight must be specified'); ? 'mobileWidth and mobileHeight must be specified'
final w = double.parse(attrs[Attribute.mobileWidth]!); : 'width and height must be specified',
final h = double.parse(attrs[Attribute.mobileHeight]!); );
imageSize = OptionalSize(w, h); imageSize = OptionalSize(width, height);
final m = attrs[Attribute.mobileMargin] == null
? 0.0
: double.parse(attrs[Attribute.mobileMargin]!);
final a = base.getAlignment(attrs[Attribute.mobileAlignment]);
image = Padding( image = Padding(
padding: EdgeInsets.all(m), padding: EdgeInsets.all(margin),
child: imageByUrl(imageUrl, width: w, height: h, alignment: a)); child: getQuillImageByUrl(
imageUrl,
width: width,
height: height,
alignment: alignment,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
),
);
} }
} }
if (imageSize == null) { if (imageSize == null) {
image = imageByUrl(imageUrl); image = getQuillImageByUrl(
imageUrl,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
);
imageSize = OptionalSize((image as Image).width, image.height); imageSize = OptionalSize((image as Image).width, image.height);
} }
@ -86,34 +130,6 @@ class ImageEmbedBuilder extends EmbedBuilder {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
final resizeOption = _SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final screenSize = MediaQuery.of(context).size;
return ImageResizer(
onImageResize: (w, h) {
final res = getEmbedNode(
controller, controller.selection.start);
final attr = base.replaceStyleString(
getImageStyleString(controller), w, h);
controller
..skipRequestKeyboard = true
..formatText(
res.offset, 1, StyleAttribute(attr));
},
imageWidth: imageSize?.width,
imageHeight: imageSize?.height,
maxWidth: screenSize.width,
maxHeight: screenSize.height);
});
},
);
final copyOption = _SimpleDialogItem( final copyOption = _SimpleDialogItem(
icon: Icons.copy_all_outlined, icon: Icons.copy_all_outlined,
color: Colors.cyanAccent, color: Colors.cyanAccent,
@ -161,9 +177,95 @@ 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, _SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final screenSize = MediaQuery.sizeOf(context);
return ImageResizer(
onImageResize: (w, h) {
final res = getEmbedNode(
controller,
controller.selection.start,
);
// For desktop
String _replaceStyleStringWithSize(
String s,
double width,
double height,
) {
final result = <String, String>{};
final pairs = s.split(';');
for (final pair in pairs) {
final _index = pair.indexOf(':');
if (_index < 0) {
continue;
}
final _key =
pair.substring(0, _index).trim();
result[_key] =
pair.substring(_index + 1).trim();
}
result[Attribute.width.key] =
width.toString();
result[Attribute.height.key] =
height.toString();
final sb = StringBuffer();
for (final pair in result.entries) {
sb
..write(pair.key)
..write(': ')
..write(pair.value)
..write('; ');
}
return sb.toString();
}
// TODO: When update flutter_quill
// we should update flutter_quill_extensions
// to use the latest version and use
// base.replaceStyleStringWithSize()
// instead of replaceStyleString
final attr = base.isMobile()
? base.replaceStyleString(
getImageStyleString(controller),
w,
h,
)
: _replaceStyleStringWithSize(
getImageStyleString(controller),
w,
h,
);
controller
..skipRequestKeyboard = true
..formatText(
res.offset,
1,
StyleAttribute(attr),
);
},
imageWidth: imageSize?.width,
imageHeight: imageSize?.height,
maxWidth: screenSize.width,
maxHeight: screenSize.height,
);
},
);
},
),
copyOption, copyOption,
removeOption, removeOption,
]), ]),
@ -179,9 +281,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 +293,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,72 +405,91 @@ 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(
context: context, context: context,
builder: (context) { builder: (context) {
final saveOption = _SimpleDialogItem( final saveOption = _SimpleDialogItem(
icon: Icons.save, icon: Icons.save,
color: Colors.greenAccent, color: Colors.greenAccent,
text: 'Save'.i18n, text: 'Save'.i18n,
onPressed: () async { onPressed: () async {
imageUrl = appendFileExtensionToImageUrl(imageUrl); imageUrl = appendFileExtensionToImageUrl(imageUrl);
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop(); Navigator.of(context).pop();
final saveImageResult = await saveImage(imageUrl); final saveImageResult = await saveImage(imageUrl);
final imageSavedSuccessfully = saveImageResult.isSuccess; final imageSavedSuccessfully = saveImageResult.isSuccess;
messenger.clearSnackBars(); messenger.clearSnackBars();
if (!imageSavedSuccessfully) { if (!imageSavedSuccessfully) {
messenger.showSnackBar(SnackBar( messenger.showSnackBar(SnackBar(
content: Text( content: Text(
'Error while saving image'.i18n, 'Error while saving image'.i18n,
))); )));
return; return;
} }
var message; var message;
switch (saveImageResult.method) { switch (saveImageResult.method) {
case SaveImageResultMethod.network: case SaveImageResultMethod.network:
message = 'Saved using the network'.i18n; message = 'Saved using the network'.i18n;
break; break;
case SaveImageResultMethod.localStorage: case SaveImageResultMethod.localStorage:
message = 'Saved using the local storage'.i18n; message = 'Saved using the local storage'.i18n;
break; break;
} }
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( SnackBar(
content: Text(message), content: Text(message),
),
);
},
);
final zoomOption = _SimpleDialogItem(
icon: Icons.zoom_in,
color: Colors.cyanAccent,
text: 'Zoom'.i18n,
onPressed: () {
Navigator.pushReplacement(
context,
// TODO: Consider add support for other theme system
// like Cupertino or at least add the option to by
// 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,
), ),
); ),
}, );
); },
final zoomOption = _SimpleDialogItem( );
icon: Icons.zoom_in, return Padding(
color: Colors.cyanAccent, padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
text: 'Zoom'.i18n, child: SimpleDialog(
onPressed: () { shape: const RoundedRectangleBorder(
Navigator.pushReplacement( borderRadius: BorderRadius.all(
context, Radius.circular(10),
MaterialPageRoute( ),
builder: (context) => ),
ImageTapWrapper(imageUrl: imageUrl))); children: [saveOption, zoomOption],
}, ),
); );
return Padding( },
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), );
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
children: [saveOption, zoomOption]),
);
});
}, },
child: image); child: image);
} }

@ -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,40 @@ 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( final selector = cameraPickSettingSelector ??
mainAxisSize: MainAxisSize.min, (context) => showDialog<MediaPickSetting>(
children: [ context: context,
builder: (ctx) => AlertDialog(
contentPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (onImagePickCallback != null)
TextButton.icon( TextButton.icon(
icon: const Icon( icon: const Icon(
Icons.camera, Icons.camera,
@ -94,6 +104,7 @@ class CameraButton extends StatelessWidget {
onPressed: () => onPressed: () =>
Navigator.pop(ctx, MediaPickSetting.Camera), Navigator.pop(ctx, MediaPickSetting.Camera),
), ),
if (onVideoPickCallback != null)
TextButton.icon( TextButton.icon(
icon: const Icon( icon: const Icon(
Icons.video_call, Icons.video_call,
@ -103,28 +114,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,
);
} }
} }
@ -219,9 +224,8 @@ class _MediaLinkDialogState extends State<MediaLinkDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final constraints = widget.dialogTheme?.linkDialogConstraints ?? final constraints = widget.dialogTheme?.linkDialogConstraints ??
() { () {
final mediaQuery = MediaQuery.of(context); final size = MediaQuery.sizeOf(context);
final maxWidth = final maxWidth = kIsWeb ? size.width / 4 : size.width - 80;
kIsWeb ? mediaQuery.size.width / 4 : mediaQuery.size.width - 80;
return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); return BoxConstraints(maxWidth: maxWidth, maxHeight: 80);
}(); }();
@ -333,13 +337,13 @@ class MediaSourceSelectorDialog extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? final constraints = dialogTheme?.mediaSelectorDialogConstraints ??
() { () {
final mediaQuery = MediaQuery.of(context); final size = MediaQuery.sizeOf(context);
double maxWidth, maxHeight; double maxWidth, maxHeight;
if (kIsWeb) { if (kIsWeb) {
maxWidth = mediaQuery.size.width / 7; maxWidth = size.width / 7;
maxHeight = mediaQuery.size.height / 7; maxHeight = size.height / 7;
} else { } else {
maxWidth = mediaQuery.size.width - 80; maxWidth = size.width - 80;
maxHeight = maxWidth / 2; maxHeight = maxWidth / 2;
} }
return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight);

@ -1,6 +1,6 @@
import 'dart:io' show File; import 'dart:io' show File;
import 'package:flutter/foundation.dart' show Uint8List; import 'package:flutter/foundation.dart' show Uint8List, immutable;
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -41,6 +41,7 @@ bool isImageBase64(String imageUrl) {
enum SaveImageResultMethod { network, localStorage } enum SaveImageResultMethod { network, localStorage }
@immutable
class _SaveImageResult { class _SaveImageResult {
const _SaveImageResult({required this.isSuccess, required this.method}); const _SaveImageResult({required this.isSuccess, required this.method});

@ -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 getQuillImageByUrl(
{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);
} }
@ -90,12 +124,16 @@ class ImageTapWrapper extends StatelessWidget {
return Scaffold( return Scaffold(
body: Container( body: Container(
constraints: BoxConstraints.expand( constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height, height: MediaQuery.sizeOf(context).height,
), ),
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,
@ -107,7 +145,7 @@ class ImageTapWrapper extends StatelessWidget {
), ),
Positioned( Positioned(
right: 10, right: 10,
top: MediaQuery.of(context).padding.top + 10.0, top: MediaQuery.paddingOf(context).top + 10.0,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);

@ -42,6 +42,11 @@ class _ImageResizerState extends State<ImageResizer> {
return _showCupertinoMenu(); return _showCupertinoMenu();
case TargetPlatform.android: case TargetPlatform.android:
return _showMaterialMenu(); return _showMaterialMenu();
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
return _showMaterialMenu();
default: default:
throw 'Not supposed to be invoked for $defaultTargetPlatform'; throw 'Not supposed to be invoked for $defaultTargetPlatform';
} }
@ -68,7 +73,11 @@ class _ImageResizerState extends State<ImageResizer> {
} }
Widget _slider( Widget _slider(
double value, double max, String label, ValueChanged<double> onChanged) { double value,
double max,
String label,
ValueChanged<double> onChanged,
) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card( child: Card(

@ -72,11 +72,35 @@ 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
/// // Don't forgot to check if that image is local or network one
/// return CachedNetworkImageProvider(imageUrl);
/// }
/// ```
///
/// [imageErrorWidgetBuilder] if you want to show a custom widget based on the
/// exception that happen while loading the image, if it network image or
/// local one, and it will get called on all the images even in the photo
/// preview widget and not just in the quill editor
/// by default the default error from flutter framework will thrown
///
/// [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 +131,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 {

@ -12,7 +12,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_quill: ^7.4.9 flutter_quill: ^7.4.13
http: ^1.1.0 http: ^1.1.0
image_picker: ">=1.0.4" image_picker: ">=1.0.4"

@ -102,6 +102,8 @@ class Attribute<T> {
static final ScriptAttribute script = ScriptAttribute(null); static final ScriptAttribute script = ScriptAttribute(null);
// TODO: You might want to mark those as key (mobileWidthKey)
// because it was not very clear to a developer that is new to this project
static const String mobileWidth = 'mobileWidth'; static const String mobileWidth = 'mobileWidth';
static const String mobileHeight = 'mobileHeight'; static const String mobileHeight = 'mobileHeight';
@ -110,6 +112,12 @@ class Attribute<T> {
static const String mobileAlignment = 'mobileAlignment'; static const String mobileAlignment = 'mobileAlignment';
/// For other platforms, for mobile use [mobileAlignment]
static const String alignment = 'alignment';
/// For other platforms, for mobile use [mobileMargin]
static const String margin = 'margin';
static const ImageAttribute image = ImageAttribute(null); static const ImageAttribute image = ImageAttribute(null);
static const VideoAttribute video = VideoAttribute(null); static const VideoAttribute video = VideoAttribute(null);

@ -59,8 +59,14 @@ class Rules {
_customRules = customRules; _customRules = customRules;
} }
Delta apply(RuleType ruleType, Document document, int index, Delta apply(
{int? len, Object? data, Attribute? attribute}) { RuleType ruleType,
Document document,
int index, {
int? len,
Object? data,
Attribute? attribute,
}) {
final delta = document.toDelta(); final delta = document.toDelta();
for (final rule in _customRules + _rules) { for (final rule in _customRules + _rules) {
if (rule.type != ruleType) { if (rule.type != ruleType) {
@ -76,6 +82,8 @@ class Rules {
rethrow; rethrow;
} }
} }
throw 'Apply rules failed'; throw FormatException(
'Apply delta rules failed. No matching rule found for type: $ruleType',
);
} }
} }

@ -206,9 +206,10 @@ extension Localization on String {
'Find text': 'بحث عن نص', 'Find text': 'بحث عن نص',
'Move to previous occurrence': 'الانتقال إلى الحدث السابق', 'Move to previous occurrence': 'الانتقال إلى الحدث السابق',
'Move to next occurrence': 'الانتقال إلى الحدث التالي', 'Move to next occurrence': 'الانتقال إلى الحدث التالي',
'Saved using the network': 'Saved using the network', 'Saved using the network': 'تم الحفظ باستخدام الشبكة',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage':
'Error while saving image': 'Error while saving image', 'تم الحفظ باستخدام وحدة التخزين المحلية',
'Error while saving image': 'حدث خطأ أثناء حفظ الصورة',
}, },
'da': { 'da': {
'Paste a link': 'Indsæt link', 'Paste a link': 'Indsæt link',

@ -1,12 +1,15 @@
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart'
show kIsWeb, TargetPlatform, defaultTargetPlatform;
bool isMobile([TargetPlatform? targetPlatform]) { bool isMobile([TargetPlatform? targetPlatform]) {
if (kIsWeb) return false;
targetPlatform ??= defaultTargetPlatform; targetPlatform ??= defaultTargetPlatform;
return {TargetPlatform.iOS, TargetPlatform.android}.contains(targetPlatform); return {TargetPlatform.iOS, TargetPlatform.android}.contains(targetPlatform);
} }
bool isDesktop([TargetPlatform? targetPlatform]) { bool isDesktop([TargetPlatform? targetPlatform]) {
if (kIsWeb) return false;
targetPlatform ??= defaultTargetPlatform; targetPlatform ??= defaultTargetPlatform;
return {TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows} return {TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows}
.contains(targetPlatform); .contains(targetPlatform);
@ -18,6 +21,7 @@ bool isKeyboardOS([TargetPlatform? targetPlatform]) {
} }
bool isAppleOS([TargetPlatform? targetPlatform]) { bool isAppleOS([TargetPlatform? targetPlatform]) {
if (kIsWeb) return false;
targetPlatform ??= defaultTargetPlatform; targetPlatform ??= defaultTargetPlatform;
return { return {
TargetPlatform.macOS, TargetPlatform.macOS,

@ -19,7 +19,26 @@ Map<String, String> parseKeyValuePairs(String s, Set<String> targetKeys) {
return result; return result;
} }
String replaceStyleString(String s, double width, double height) { @Deprecated('Use replaceStyleStringWithSize instead')
String replaceStyleString(
String s,
double width,
double height,
) {
return replaceStyleStringWithSize(
s,
width: width,
height: height,
isMobile: true,
);
}
String replaceStyleStringWithSize(
String s, {
required double width,
required double height,
required bool isMobile,
}) {
final result = <String, String>{}; final result = <String, String>{};
final pairs = s.split(';'); final pairs = s.split(';');
for (final pair in pairs) { for (final pair in pairs) {
@ -31,8 +50,13 @@ String replaceStyleString(String s, double width, double height) {
result[_key] = pair.substring(_index + 1).trim(); result[_key] = pair.substring(_index + 1).trim();
} }
result[Attribute.mobileWidth] = width.toString(); if (isMobile) {
result[Attribute.mobileHeight] = height.toString(); result[Attribute.mobileWidth] = width.toString();
result[Attribute.mobileHeight] = height.toString();
} else {
result[Attribute.width.key] = width.toString();
result[Attribute.height.key] = height.toString();
}
final sb = StringBuffer(); final sb = StringBuffer();
for (final pair in result.entries) { for (final pair in result.entries) {
sb sb

@ -497,7 +497,7 @@ class QuillEditorState extends State<QuillEditor>
cupertinoTheme.primaryColor.withOpacity(0.40); cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2); cursorRadius ??= const Radius.circular(2);
cursorOffset = Offset( cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
} else { } else {
textSelectionControls = materialTextSelectionControls; textSelectionControls = materialTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;

@ -977,7 +977,7 @@ class RawEditorState extends EditorState
widget.selectionColor, widget.selectionColor,
widget.enableInteractiveSelection, widget.enableInteractiveSelection,
_hasFocus, _hasFocus,
MediaQuery.of(context).devicePixelRatio, MediaQuery.devicePixelRatioOf(context),
_cursorCont); _cursorCont);
return editableTextLine; return editableTextLine;
} }

@ -159,7 +159,7 @@ class EditableTextBlock extends StatelessWidget {
color, color,
enableInteractiveSelection, enableInteractiveSelection,
hasFocus, hasFocus,
MediaQuery.of(context).devicePixelRatio, MediaQuery.devicePixelRatioOf(context),
cursorCont); cursorCont);
final nodeTextDirection = getDirectionOfNode(line); final nodeTextDirection = getDirectionOfNode(line);
children.add(Directionality( children.add(Directionality(

@ -245,9 +245,8 @@ class _LinkStyleDialogState extends State<LinkStyleDialog> {
final constraints = widget.constraints ?? final constraints = widget.constraints ??
widget.dialogTheme?.linkDialogConstraints ?? widget.dialogTheme?.linkDialogConstraints ??
() { () {
final mediaQuery = MediaQuery.of(context); final size = MediaQuery.sizeOf(context);
final maxWidth = final maxWidth = kIsWeb ? size.width / 4 : size.width - 80;
kIsWeb ? mediaQuery.size.width / 4 : mediaQuery.size.width - 80;
return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); return BoxConstraints(maxWidth: maxWidth, maxHeight: 80);
}(); }();

@ -1,6 +1,6 @@
name: flutter_quill name: flutter_quill
description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter.
version: 7.4.13 version: 7.4.14
homepage: https://1o24bbs.com/c/bulletjournal/108 homepage: https://1o24bbs.com/c/bulletjournal/108
repository: https://github.com/singerdmx/flutter-quill repository: https://github.com/singerdmx/flutter-quill

Loading…
Cancel
Save