Update flutter_quill_extensions part 2

pull/1519/head
Ellet 1 year ago
parent ebd2729f39
commit c240237ed2
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 3
      CHANGELOG.md
  2. 9
      README.md
  3. 2
      doc/CONTRIBUTING.md
  4. 11
      doc/todo.md
  5. 21
      example/assets/sample_data_testing.json
  6. 5
      example/lib/pages/home_page.dart
  7. 1
      example/pubspec.yaml
  8. 8
      flutter_quill_extensions/CHANGELOG.md
  9. 8
      flutter_quill_extensions/README.md
  10. 445
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart
  11. 195
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart
  12. 5
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart
  13. 11
      flutter_quill_extensions/lib/presentation/embeds/utils.dart
  14. 73
      flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart
  15. 1
      flutter_quill_extensions/lib/presentation/models/config/editor/image/image.dart
  16. 2
      flutter_quill_extensions/pubspec.yaml
  17. 11
      lib/src/models/structs/optional_size.dart
  18. 7
      lib/src/utils/string.dart
  19. 17
      lib/src/widgets/text_line.dart
  20. 2
      pubspec.yaml

@ -1,3 +1,6 @@
## [8.4.1]
- Add `copyWith` in `OptionalSize` class
## [8.4.0] ## [8.4.0]
- **Breaking change**: Update the `QuillCustomButton` to have `QuillCustomButtonOptions`. We moved everything that is in the `QuillCustomButton` to `QuillCustomButtonOptions` but replaced the `iconData` with `icon` widget for more customizations - **Breaking change**: Update the `QuillCustomButton` to have `QuillCustomButtonOptions`. We moved everything that is in the `QuillCustomButton` to `QuillCustomButtonOptions` but replaced the `iconData` with `icon` widget for more customizations
- **Breaking change**: the `customButtons` in the `QuillToolbarConfigurations` is now of type `List<QuillToolbarCustomButtonOptions>` - **Breaking change**: the `customButtons` in the `QuillToolbarConfigurations` is now of type `List<QuillToolbarCustomButtonOptions>`

@ -66,10 +66,13 @@ dependencies:
git: https://github.com/singerdmx/flutter-quill.git git: https://github.com/singerdmx/flutter-quill.git
``` ```
>
> Note: At this time, we are making too many changes to the library and you might see new version almost every day
> >
> Using the latest version and reporting any issues you encounter on GitHub will greatly contribute to the improvement of the library. Your input and insights are valuable in shaping a stable and reliable version for all our users. Thank you for being part of the open-source community! > Using the latest version and reporting any issues you encounter on GitHub will greatly contribute to the improvement of the library. Your input and insights are valuable in shaping a stable and reliable version for all our users. Thank you for being part of the open-source community!
> >
> Please use the latest pre-release of [FlutterQuill Extensions] in order to work with the latest stable version of [FlutterQuill] > If the latest version of [FlutterQuill Extensions] is pre-release, then please use it in order to work with the latest stable version of [FlutterQuill]
> >
## Usage ## Usage
@ -297,7 +300,9 @@ Made with [contrib.rocks](https://contrib.rocks).
We welcome contributions! We welcome contributions!
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./doc/CONTRIBUTING.md) for more details. Please follow these guidelines when contributing to the project. See [CONTRIBUTING.md](./doc/CONTRIBUTING.md) for more details. <br>
You can check the [Todo](./doc/todo.md) list if you want to
[Quill]: https://quilljs.com/docs/formats [Quill]: https://quilljs.com/docs/formats
[Flutter]: https://github.com/flutter/flutter [Flutter]: https://github.com/flutter/flutter

@ -3,6 +3,8 @@
The contributions are more than welcome! <br> The contributions are more than welcome! <br>
This project will be better with the open-source community help This project will be better with the open-source community help
You can check the [Todo](./todo.md) list if you want to
There are no guidelines for now. There are no guidelines for now.
This page will be updated in the future. This page will be updated in the future.

@ -0,0 +1,11 @@
# Todo
This is a todo list page that added recently and will be updated soon.
## Flutter Quill
1. Improve the Raw Quill Editor
2. Provide more support to all the platforms
## Flutter Quill Extensions
1. Add support for cropping an image in `flutter_quill_extensions`

@ -1,6 +1,25 @@
[ [
{ {
"insert": "Here is an image: \n" "insert": "Here is an local image: \n"
},
{
"insert": "\n"
},
{
"insert": {
"image": "assets/images/1.png"
},
"attributes": {
"width": "100",
"height": "100",
"style": "width:500px; height:350px;"
}
},
{
"insert": "\n"
},
{
"insert": "Here is an network image: \n"
}, },
{ {
"insert": "\n" "insert": "\n"

@ -444,9 +444,8 @@ class _HomePageState extends State<HomePage> {
), ),
embedBuilders: [ embedBuilders: [
...FlutterQuillEmbeds.editorBuilders( ...FlutterQuillEmbeds.editorBuilders(
imageEmbedConfigurations: const QuillEditorImageEmbedConfigurations( imageEmbedConfigurations:
forceUseMobileOptionMenuForImageClick: true, const QuillEditorImageEmbedConfigurations(),
),
), ),
TimeStampEmbedBuilderWidget() TimeStampEmbedBuilderWidget()
], ],

@ -41,6 +41,7 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/ - assets/
- assets/images/
fonts: fonts:
- family: monospace - family: monospace

@ -1,5 +1,13 @@
## 0.6.5 ## 0.6.5
- Support the new improved platform checking of `flutter_quill` - Support the new improved platform checking of `flutter_quill`
- Update the Image embed builder logic
- Fix Save image button exception
- Feature: Image cropping for the image embed builder
- Add support for copying the image to the cliboard
- Fix the image size logic (it's still missing a lot of things but we will work on that soon)
- Fix the zoom image functionality to support different image providers
- Update `README.md`
- Deprecated: The boolean property `forceUseMobileOptionMenuForImageClick` is now deprecated as we will not using it anymore and it will be removed in the next major release
## 0.6.4 ## 0.6.4
- Update `QuillImageUtilities` - Update `QuillImageUtilities`

@ -17,7 +17,7 @@ Currently the support for **Web** is limitied.
- [Usage](#usage) - [Usage](#usage)
- [Embed Blocks](#embed-blocks) - [Embed Blocks](#embed-blocks)
- [Custom Size Image for Mobile](#custom-size-image-for-mobile) - [Custom Size Image for Mobile](#custom-size-image-for-mobile)
- [Custom Size Image for other platforms (excluding web)](#custom-size-image-for-other-platforms-excluding-web) - [Custom Size Image for other platforms](#custom-size-image-for-other-platforms)
- [Drag and drop feature](#drag-and-drop-feature) - [Drag and drop feature](#drag-and-drop-feature)
- [Features](#features) - [Features](#features)
- [Contributing](#contributing) - [Contributing](#contributing)
@ -135,7 +135,7 @@ Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follo
} }
``` ```
### Custom Size Image for other platforms (excluding web) ### Custom Size Image for other platforms
Define `width`, `height`, `margin`, `alignment` as follows: Define `width`, `height`, `margin`, `alignment` as follows:
@ -150,9 +150,7 @@ Define `width`, `height`, `margin`, `alignment` as follows:
} }
``` ```
On mobile we will use `mobileWidth`, `mobileHeight`, on desktop will use `width`, `heigth`
on Web we will use the `width` and the `height` but the ones in the `attributes`
This may not clear but don't worry we will update it soon.
### Drag and drop feature ### Drag and drop feature
Currently the drag and drop feature is not offically supported but you can achieve this very easily in the following steps: Currently the drag and drop feature is not offically supported but you can achieve this very easily in the following steps:

@ -1,16 +1,12 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' as base; import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize;
import 'package:flutter_quill/translations.dart';
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../models/config/editor/image/image.dart'; import '../../../models/config/editor/image/image.dart';
import '../../embed_types/image.dart';
import '../../utils.dart';
import '../../widgets/image.dart'; import '../../widgets/image.dart';
import '../../widgets/image_resizer.dart'; import 'image_menu.dart';
import '../../widgets/simple_dialog_item.dart';
class QuillEditorImageEmbedBuilder extends EmbedBuilder { class QuillEditorImageEmbedBuilder extends EmbedBuilder {
QuillEditorImageEmbedBuilder({ QuillEditorImageEmbedBuilder({
@ -35,301 +31,214 @@ class QuillEditorImageEmbedBuilder extends EmbedBuilder {
) { ) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
Widget image = const SizedBox.shrink(); final imageSource = standardizeImageUrl(node.value.data);
final imageUrl = standardizeImageUrl(node.value.data); final ((imageSize), margin, alignment) = _getImageAttributes(node);
OptionalSize? imageSize;
final style = node.style.attributes['style'];
if (style != null) { final width = imageSize.width;
final attrs = base.isMobile(supportWeb: false) final height = imageSize.height;
? base.parseKeyValuePairs(style.value.toString(), {
Attribute.mobileWidth,
Attribute.mobileHeight,
Attribute.mobileMargin,
Attribute.mobileAlignment,
})
: base.parseKeyValuePairs(style.value.toString(), {
Attribute.width.key,
Attribute.height.key,
Attribute.margin,
Attribute.alignment,
});
if (attrs.isNotEmpty) {
final width = double.tryParse(
(base.isMobile(supportWeb: false)
? attrs[Attribute.mobileWidth]
: attrs[Attribute.width.key]) ??
'',
);
final height = double.tryParse(
(base.isMobile(supportWeb: false)
? attrs[Attribute.mobileHeight]
: attrs[Attribute.height.key]) ??
'',
);
final alignment = base.getAlignment(base.isMobile(supportWeb: false)
? attrs[Attribute.mobileAlignment]
: attrs[Attribute.alignment]);
final margin = (base.isMobile(supportWeb: false)
? double.tryParse(Attribute.mobileMargin)
: double.tryParse(Attribute.margin)) ??
0.0;
// assert( final image = getImageWidgetByImageSource(
// width != null && height != null, imageSource,
// base.isMobile()
// ? 'mobileWidth and mobileHeight must be specified'
// : 'width and height must be specified',
// );
imageSize = OptionalSize(width, height);
image = Padding(
padding: EdgeInsets.all(margin),
child: getImageWidgetByImageSource(
imageUrl,
width: width,
height: height,
alignment: alignment,
imageProviderBuilder: configurations.imageProviderBuilder, imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
), alignment: alignment,
height: height,
width: width,
); );
}
}
if (imageSize == null) { // OptionalSize? imageSize;
image = getImageWidgetByImageSource( // final style = node.style.attributes['style'];
imageUrl,
imageProviderBuilder: configurations.imageProviderBuilder, // if (style != null) {
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, // final attrs = base.isMobile(supportWeb: false)
); // ? base.parseKeyValuePairs(style.value.toString(), {
imageSize = OptionalSize((image as Image).width, image.height); // Attribute.mobileWidth,
} // Attribute.mobileHeight,
// Attribute.mobileMargin,
// Attribute.mobileAlignment,
// })
// : base.parseKeyValuePairs(style.value.toString(), {
// Attribute.width.key,
// Attribute.height.key,
// Attribute.margin,
// Attribute.alignment,
// });
// if (attrs.isNotEmpty) {
// final width = double.tryParse(
// (base.isMobile(supportWeb: false)
// ? attrs[Attribute.mobileWidth]
// : attrs[Attribute.width.key]) ??
// '',
// );
// final height = double.tryParse(
// (base.isMobile(supportWeb: false)
// ? attrs[Attribute.mobileHeight]
// : attrs[Attribute.height.key]) ??
// '',
// );
// final alignment = base.getAlignment(base.isMobile(supportWeb: false)
// ? attrs[Attribute.mobileAlignment]
// : attrs[Attribute.alignment]);
// final margin = (base.isMobile(supportWeb: false)
// ? double.tryParse(Attribute.mobileMargin)
// : double.tryParse(Attribute.margin)) ??
// 0.0;
// imageSize = OptionalSize(width, height);
// image = Padding(
// padding: EdgeInsets.all(margin),
// child: getImageWidgetByImageSource(
// imageSource,
// width: width,
// height: height,
// alignment: alignment,
// imageProviderBuilder: configurations.imageProviderBuilder,
// imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
// ),
// );
// }
// }
// if (imageSize == null) {
// image = getImageWidgetByImageSource(
// imageSource,
// imageProviderBuilder: configurations.imageProviderBuilder,
// imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
// );
// imageSize = OptionalSize((image as Image).width, image.height);
// }
if (!readOnly && final imageSaverService =
(base.isMobile(supportWeb: false) || QuillSharedExtensionsConfigurations.get(context: context)
configurations.forceUseMobileOptionMenuForImageClick)) { .imageSaverService;
return GestureDetector( return GestureDetector(
onTap: () { child: Builder(
showDialog(
context: context,
builder: (context) { builder: (context) {
final copyOption = SimpleDialogItem( if (margin != null) {
icon: Icons.copy_all_outlined, return Padding(
color: Colors.cyanAccent, padding: EdgeInsets.all(margin),
text: 'Copy'.i18n, child: image,
onPressed: () {
final imageNode =
getEmbedNode(controller, controller.selection.start)
.value;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl = ImageUrl(
imageUrl,
getImageStyleString(controller),
);
Navigator.pop(context);
},
); );
final removeOption = SimpleDialogItem(
icon: Icons.delete_forever_outlined,
color: Colors.red.shade200,
text: 'Remove'.i18n,
onPressed: () async {
Navigator.of(context).pop();
// Call the remove check callback if set
if (await configurations.shouldRemoveImageCallback
?.call(imageUrl) ==
false) {
return;
} }
return image;
final offset = getEmbedNode(
controller,
controller.selection.start,
).offset;
controller.replaceText(
offset,
1,
'',
TextSelection.collapsed(offset: offset),
);
// Call the post remove callback if set
await configurations.onImageRemovedCallback.call(imageUrl);
}, },
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
), ),
children: [ onTap: () => showDialog(
SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {
final screenSize = MediaQuery.sizeOf(context); return ImageOptionsMenu(
return ImageResizer( controller: controller,
onImageResize: (w, h) { configurations: configurations,
final res = getEmbedNode( imageSource: imageSource,
controller, imageSize: imageSize,
controller.selection.start, isReadOnly: readOnly,
); imageSaverService: imageSaverService,
final attr =
base.replaceStyleStringWithSize(
getImageStyleString(controller),
width: w,
height: h,
isMobile:
base.isMobile(supportWeb: false),
);
controller
..skipRequestKeyboard = true
..formatText(
res.offset,
1,
StyleAttribute(attr),
);
},
imageWidth: imageSize?.width,
imageHeight: imageSize?.height,
maxWidth: screenSize.width,
maxHeight: screenSize.height,
);
},
); );
}, },
), ),
copyOption,
removeOption,
]),
);
});
},
child: image,
); );
} }
}
if (!readOnly || isImageBase64(imageUrl)) { (
// To enforce using it on the desktop and other platforms OptionalSize imageSize,
// and that is up to the developer double? margin,
if (!base.isMobile(supportWeb: false) && Alignment alignment,
configurations.forceUseMobileOptionMenuForImageClick) { ) _getImageAttributes(
return _menuOptionsForReadonlyImage( Node node,
context: context, ) {
imageUrl: imageUrl, var imageSize = const OptionalSize(null, null);
image: image, var imageAlignment = Alignment.center;
imageProviderBuilder: configurations.imageProviderBuilder, double? imageMargin;
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
// Usually double value
final heightValue = double.tryParse(
node.style.attributes[Attribute.height.key]?.value.toString() ?? '');
final widthValue = double.tryParse(
node.style.attributes[Attribute.width.key]?.value.toString() ?? '');
if (heightValue != null) {
imageSize = imageSize.copyWith(
height: heightValue,
); );
} }
return image; if (widthValue != null) {
} imageSize = imageSize.copyWith(
width: widthValue,
// We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage(
context: context,
imageUrl: imageUrl,
image: image,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
); );
} }
final cssStyle = node.style.attributes['style'];
if (cssStyle != null) {
final attrs = base.isMobile(supportWeb: false)
? base.parseKeyValuePairs(cssStyle.value.toString(), {
Attribute.mobileWidth,
Attribute.mobileHeight,
Attribute.mobileMargin,
Attribute.mobileAlignment,
})
: base.parseKeyValuePairs(cssStyle.value.toString(), {
Attribute.width.key,
Attribute.height.key,
Attribute.margin,
Attribute.alignment,
});
if (attrs.isEmpty) {
return (imageSize, imageMargin, imageAlignment);
} }
Widget _menuOptionsForReadonlyImage({ // It css value as string but we will try to support it anyway
required BuildContext context,
required String imageUrl,
required Widget image,
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
}) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (_) {
final saveOption = SimpleDialogItem(
icon: Icons.save,
color: Colors.greenAccent,
text: 'Save'.i18n,
onPressed: () async {
imageUrl = appendFileExtensionToImageUrl(imageUrl);
final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop();
final saveImageResult = await saveImage( // TODO: This could be improved much better
imageUrl: imageUrl, final cssHeightValue = double.tryParse(
context: context, (attrs[Attribute.height.key] ?? '').replaceFirst('px', ''));
); final cssWidthValue = double.tryParse(
final imageSavedSuccessfully = saveImageResult.isSuccess; (attrs[Attribute.width.key] ?? '').replaceFirst('px', ''));
messenger.clearSnackBars(); if (cssHeightValue != null) {
imageSize = imageSize.copyWith(height: cssHeightValue);
}
if (cssWidthValue != null) {
imageSize = imageSize.copyWith(width: cssWidthValue);
}
if (!imageSavedSuccessfully) { imageAlignment = base.getAlignment(base.isMobile(supportWeb: false)
messenger.showSnackBar(SnackBar( ? attrs[Attribute.mobileAlignment]
content: Text( : attrs[Attribute.alignment]);
'Error while saving image'.i18n, final margin = (base.isMobile(supportWeb: false)
))); ? double.tryParse(Attribute.mobileMargin)
return; : double.tryParse(Attribute.margin));
if (margin != null) {
imageMargin = margin;
}
} }
String message; return (imageSize, imageMargin, imageAlignment);
switch (saveImageResult.method) {
case SaveImageResultMethod.network:
message = 'Saved using the network'.i18n;
break;
case SaveImageResultMethod.localStorage:
message = 'Saved using the local storage'.i18n;
break;
} }
messenger.showSnackBar( @immutable
SnackBar( class OptionalSize {
content: Text(message), const OptionalSize(
), this.width,
); this.height,
},
);
final zoomOption = SimpleDialogItem(
icon: Icons.zoom_in,
color: Colors.cyanAccent,
text: 'Zoom'.i18n,
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ImageTapWrapper(
imageUrl: imageUrl,
imageProviderBuilder: imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder,
),
),
);
},
);
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, /// If non-null, requires the child to have exactly this width.
/// If null, the child is free to choose its own width.
final double? width;
/// If non-null, requires the child to have exactly this height.
/// If null, the child is free to choose its own height.
final double? height;
OptionalSize copyWith({
double? width,
double? height,
}) {
return OptionalSize(
width ?? this.width,
height ?? this.height,
); );
} }
}

@ -0,0 +1,195 @@
import 'package:flutter/cupertino.dart' show showCupertinoModalPopup;
import 'package:flutter/material.dart';
// import 'package:flutter/services.dart' show Clipboard, ClipboardData;
import 'package:flutter_quill/extensions.dart'
show isDesktop, isMobile, replaceStyleStringWithSize;
import 'package:flutter_quill/flutter_quill.dart'
show ImageUrl, QuillController, StyleAttribute, getEmbedNode;
import 'package:flutter_quill/translations.dart';
import '../../../../logic/services/image_saver/s_image_saver.dart';
import '../../../models/config/editor/image/image.dart';
import '../../utils.dart';
import '../../widgets/image.dart' show ImageTapWrapper, getImageStyleString;
import '../../widgets/image_resizer.dart' show ImageResizer;
import 'image.dart' show OptionalSize;
class ImageOptionsMenu extends StatelessWidget {
const ImageOptionsMenu({
required this.controller,
required this.configurations,
required this.imageSource,
required this.imageSize,
required this.isReadOnly,
required this.imageSaverService,
super.key,
});
final QuillController controller;
final QuillEditorImageEmbedConfigurations configurations;
final String imageSource;
final OptionalSize imageSize;
final bool isReadOnly;
final ImageSaverService imageSaverService;
@override
Widget build(BuildContext context) {
final materialTheme = Theme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
title: Text('Image'.i18n),
children: [
if (!isReadOnly)
ListTile(
title: Text('Resize'.i18n),
leading: const Icon(Icons.settings_outlined),
onTap: () {
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,
);
final attr = replaceStyleStringWithSize(
getImageStyleString(controller),
width: w,
height: h,
isMobile: isMobile(supportWeb: false),
);
controller
..skipRequestKeyboard = true
..formatText(
res.offset,
1,
StyleAttribute(attr),
);
},
imageWidth: imageSize.width,
imageHeight: imageSize.height,
maxWidth: screenSize.width,
maxHeight: screenSize.height,
);
},
);
},
),
ListTile(
leading: const Icon(Icons.copy_all_outlined),
title: Text('Copy'.i18n),
onTap: () async {
final navigator = Navigator.of(context);
final imageNode =
getEmbedNode(controller, controller.selection.start).value;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl = ImageUrl(
imageUrl,
getImageStyleString(controller),
);
// TODO: Implement the copy image
// await Clipboard.setData(
// ClipboardData(text: '$imageUrl'),
// );
navigator.pop();
},
),
if (!isReadOnly)
ListTile(
leading: Icon(
Icons.delete_forever_outlined,
color: materialTheme.colorScheme.error,
),
title: Text('Remove'.i18n),
onTap: () async {
Navigator.of(context).pop();
// Call the remove check callback if set
if (await configurations.shouldRemoveImageCallback
?.call(imageSource) ==
false) {
return;
}
final offset = getEmbedNode(
controller,
controller.selection.start,
).offset;
controller.replaceText(
offset,
1,
'',
TextSelection.collapsed(offset: offset),
);
// Call the post remove callback if set
await configurations.onImageRemovedCallback.call(imageSource);
},
),
...[
ListTile(
leading: const Icon(Icons.save),
title: Text('Save'.i18n),
enabled: !isDesktop(supportWeb: false),
onTap: () async {
final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop();
final saveImageResult = await saveImage(
imageUrl: imageSource,
imageSaverService: imageSaverService,
);
final imageSavedSuccessfully = saveImageResult.isSuccess;
messenger.clearSnackBars();
if (!imageSavedSuccessfully) {
messenger.showSnackBar(SnackBar(
content: Text(
'Error while saving image'.i18n,
)));
return;
}
String message;
switch (saveImageResult.method) {
case SaveImageResultMethod.network:
message = 'Saved using the network'.i18n;
break;
case SaveImageResultMethod.localStorage:
message = 'Saved using the local storage'.i18n;
break;
}
messenger.showSnackBar(
SnackBar(
content: Text(message),
),
);
},
),
ListTile(
leading: const Icon(Icons.zoom_in),
title: Text('Zoom'.i18n),
onTap: () => Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ImageTapWrapper(
imageUrl: imageSource,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder:
configurations.imageErrorWidgetBuilder,
),
),
),
),
],
],
),
);
}
}

@ -33,7 +33,7 @@ class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
) { ) {
assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform');
final (height, width, margin, alignment) = _getImageSizeForWeb(node); final (height, width, margin, alignment) = _getImageWebAttributes(node);
var imageSource = node.value.data.toString(); var imageSource = node.value.data.toString();
@ -77,11 +77,12 @@ class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
String width, String width,
String margin, String margin,
String alignment, String alignment,
) _getImageSizeForWeb( ) _getImageWebAttributes(
Node node, Node node,
) { ) {
var height = 'auto'; var height = 'auto';
var width = 'auto'; var width = 'auto';
// TODO: Add support for margin and alignment
const margin = 'auto'; const margin = 'auto';
const alignment = 'center'; const alignment = 'center';

@ -3,6 +3,8 @@ import 'dart:io' show File;
import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/foundation.dart' show immutable;
import 'package:flutter/widgets.dart' show BuildContext; import 'package:flutter/widgets.dart' show BuildContext;
import '../../logic/models/config/shared_configurations.dart'; import '../../logic/models/config/shared_configurations.dart';
import '../../logic/services/image_saver/s_image_saver.dart';
import 'widgets/image.dart';
RegExp _base64 = RegExp( RegExp _base64 = RegExp(
r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$', r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
@ -49,11 +51,8 @@ class SaveImageResult {
Future<SaveImageResult> saveImage({ Future<SaveImageResult> saveImage({
required String imageUrl, required String imageUrl,
required BuildContext context, required ImageSaverService imageSaverService,
}) async { }) async {
final imageSaverService =
QuillSharedExtensionsConfigurations.get(context: context)
.imageSaverService;
final imageFile = File(imageUrl); final imageFile = File(imageUrl);
final hasPermission = await imageSaverService.hasAccess(); final hasPermission = await imageSaverService.hasAccess();
if (!hasPermission) { if (!hasPermission) {
@ -63,13 +62,15 @@ Future<SaveImageResult> saveImage({
if (!imageExistsLocally) { if (!imageExistsLocally) {
try { try {
await imageSaverService.saveImageFromNetwork( await imageSaverService.saveImageFromNetwork(
Uri.parse(imageUrl), Uri.parse(appendFileExtensionToImageUrl(imageUrl)),
); );
return const SaveImageResult( return const SaveImageResult(
isSuccess: true, isSuccess: true,
method: SaveImageResultMethod.network, method: SaveImageResultMethod.network,
); );
} catch (e) { } catch (e) {
print(e);
print(StackTrace.current);
return const SaveImageResult( return const SaveImageResult(
isSuccess: false, isSuccess: false,
method: SaveImageResultMethod.network, method: SaveImageResultMethod.network,

@ -28,6 +28,31 @@ String getImageStyleString(QuillController controller) {
return s ?? ''; return s ?? '';
} }
/// [imageProviderBuilder] To override the return value pass value to it
/// [imageSource] The source of the image in the quill delta json document
/// It could be http, file, network, asset, or base 64 image
ImageProvider getImageProviderByImageSource(
String imageSource, {
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
}) {
if (imageProviderBuilder != null) {
return imageProviderBuilder(imageSource);
}
if (isImageBase64(imageSource)) {
return MemoryImage(base64.decode(imageSource));
}
if (isHttpBasedUrl(imageSource)) {
return NetworkImage(imageSource);
}
if (imageSource.startsWith('assets')) {
// TODO: This impl needs to be improved
return AssetImage(imageSource);
}
return FileImage(File(imageSource));
}
Image getImageWidgetByImageSource( Image getImageWidgetByImageSource(
String imageSource, { String imageSource, {
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
@ -36,35 +61,11 @@ Image getImageWidgetByImageSource(
double? height, double? height,
AlignmentGeometry alignment = Alignment.center, AlignmentGeometry alignment = Alignment.center,
}) { }) {
if (isImageBase64(imageSource)) {
return Image.memory(
base64.decode(imageSource),
width: width,
height: height,
alignment: alignment,
);
}
if (imageProviderBuilder != null) {
return Image( return Image(
image: imageProviderBuilder(imageSource), image: getImageProviderByImageSource(
width: width,
height: height,
alignment: alignment,
errorBuilder: imageErrorWidgetBuilder,
);
}
if (isHttpBasedUrl(imageSource)) {
return Image.network(
imageSource, imageSource,
width: width, imageProviderBuilder: imageProviderBuilder,
height: height, ),
alignment: alignment,
errorBuilder: imageErrorWidgetBuilder,
);
}
return Image.file(
File(imageSource),
width: width, width: width,
height: height, height: height,
alignment: alignment, alignment: alignment,
@ -110,20 +111,6 @@ class ImageTapWrapper extends StatelessWidget {
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
ImageProvider _imageProviderByUrl(
String imageUrl, {
required ImageEmbedBuilderProviderBuilder? customImageProviderBuilder,
}) {
if (customImageProviderBuilder != null) {
return customImageProviderBuilder(imageUrl);
}
if (isHttpBasedUrl(imageUrl)) {
return NetworkImage(imageUrl);
}
return FileImage(File(imageUrl));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -134,9 +121,9 @@ class ImageTapWrapper extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
PhotoView( PhotoView(
imageProvider: _imageProviderByUrl( imageProvider: getImageProviderByImageSource(
imageUrl, imageUrl,
customImageProviderBuilder: imageProviderBuilder, imageProviderBuilder: imageProviderBuilder,
), ),
errorBuilder: imageErrorWidgetBuilder, errorBuilder: imageErrorWidgetBuilder,
loadingBuilder: (context, event) { loadingBuilder: (context, event) {

@ -12,6 +12,7 @@ import '../../../../embeds/embed_types/image.dart';
@immutable @immutable
class QuillEditorImageEmbedConfigurations { class QuillEditorImageEmbedConfigurations {
const QuillEditorImageEmbedConfigurations({ const QuillEditorImageEmbedConfigurations({
@Deprecated('This will be deleted in 0.7.0 as we will have one menu')
this.forceUseMobileOptionMenuForImageClick = false, this.forceUseMobileOptionMenuForImageClick = false,
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
this.shouldRemoveImageCallback, this.shouldRemoveImageCallback,

@ -1,6 +1,6 @@
name: flutter_quill_extensions name: flutter_quill_extensions
description: Embed extensions for flutter_quill including image, video, formula and etc. description: Embed extensions for flutter_quill including image, video, formula and etc.
version: 0.6.4 version: 0.6.5
homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions
repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions

@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
@immutable @immutable
@ -14,4 +15,14 @@ class OptionalSize {
/// If non-null, requires the child to have exactly this height. /// If non-null, requires the child to have exactly this height.
/// If null, the child is free to choose its own height. /// If null, the child is free to choose its own height.
final double? height; final double? height;
OptionalSize copyWith({
double? width,
double? height,
}) {
return OptionalSize(
width ?? this.width,
height ?? this.height,
);
}
} }

@ -54,9 +54,10 @@ String replaceStyleStringWithSize(
return sb.toString(); return sb.toString();
} }
Alignment getAlignment(String? s) { /// Get flutter [Alignment] value by [cssAlignment]
Alignment getAlignment(String? cssAlignment) {
const defaultAlignment = Alignment.center; const defaultAlignment = Alignment.center;
if (s == null) { if (cssAlignment == null) {
return defaultAlignment; return defaultAlignment;
} }
@ -70,7 +71,7 @@ Alignment getAlignment(String? s) {
'bottomLeft', 'bottomLeft',
'bottomCenter', 'bottomCenter',
'bottomRight' 'bottomRight'
].indexOf(s); ].indexOf(cssAlignment);
if (index < 0) { if (index < 0) {
return defaultAlignment; return defaultAlignment;
} }

@ -143,15 +143,23 @@ class _TextLineState extends State<TextLine> {
var embed = widget.line.children.single as Embed; var embed = widget.line.children.single as Embed;
// Creates correct node for custom embed // Creates correct node for custom embed
if (embed.value.type == BlockEmbed.customType) { if (embed.value.type == BlockEmbed.customType) {
embed = Embed(CustomBlockEmbed.fromJsonString(embed.value.data)); embed = Embed(
CustomBlockEmbed.fromJsonString(embed.value.data),
);
} }
final embedBuilder = widget.embedBuilder(embed); final embedBuilder = widget.embedBuilder(embed);
if (embedBuilder.expanded) { if (embedBuilder.expanded) {
// Creates correct node for custom embed // Creates correct node for custom embed
final lineStyle = _getLineStyle(widget.styles); final lineStyle = _getLineStyle(widget.styles);
return EmbedProxy( return EmbedProxy(
embedBuilder.build(context, widget.controller, embed, widget.readOnly, embedBuilder.build(
false, lineStyle), context,
widget.controller,
embed,
widget.readOnly,
false,
lineStyle,
),
); );
} }
} }
@ -172,7 +180,8 @@ class _TextLineState extends State<TextLine> {
textDirection: widget.textDirection!, textDirection: widget.textDirection!,
strutStyle: strutStyle, strutStyle: strutStyle,
locale: Localizations.localeOf(context), locale: Localizations.localeOf(context),
child: child); child: child,
);
} }
InlineSpan _getTextSpanForWholeLine(BuildContext context) { InlineSpan _getTextSpanForWholeLine(BuildContext context) {

@ -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: 8.4.0 version: 8.4.1
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