Update flutter_quill_extensions part 2 (#1519)

* Update flutter_quill_extensions part 2
pull/1520/head
Ellet 1 year ago committed by GitHub
parent ebd2729f39
commit c769b14463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 9
      README.md
  3. 2
      doc/CONTRIBUTING.md
  4. 48
      doc/todo.md
  5. 21
      example/assets/sample_data_testing.json
  6. 7
      example/lib/pages/home_page.dart
  7. 4
      example/lib/pages/read_only_page.dart
  8. 1
      example/pubspec.yaml
  9. 10
      flutter_quill_extensions/CHANGELOG.md
  10. 8
      flutter_quill_extensions/README.md
  11. 20
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  12. 489
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart
  13. 195
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart
  14. 5
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart
  15. 11
      flutter_quill_extensions/lib/presentation/embeds/utils.dart
  16. 82
      flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart
  17. 2
      flutter_quill_extensions/lib/presentation/models/config/editor/image/image.dart
  18. 2
      flutter_quill_extensions/pubspec.yaml
  19. 11
      lib/src/models/structs/optional_size.dart
  20. 7
      lib/src/utils/string.dart
  21. 27
      lib/src/widgets/text_line.dart
  22. 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,48 @@
# Todo
This is a todo list page that added recently and will be updated soon.
## Table of contents
- [Todo](#todo)
- [Table of contents](#table-of-contents)
- [Flutter Quill](#flutter-quill)
- [Features](#features)
- [Improvemenets](#improvemenets)
- [Bugs](#bugs)
- [Flutter Quill Extensions](#flutter-quill-extensions)
- [Features](#features-1)
- [Improvemenets](#improvemenets-1)
- [Bugs](#bugs-1)
## Flutter Quill
### Features
1. Add a method to set TextInputAction, fore more [info](https://github.com/singerdmx/flutter-quill/issues/1328)
2. Add support for Text magnification feature, for more [info](https://github.com/singerdmx/flutter-quill/issues/1504)
3. Provide a way to expose quills undo redo stacks, for more [info](https://github.com/singerdmx/flutter-quill/issues/1381)
### Improvemenets
1. Improve the Raw Quill Editor, for more [info](https://github.com/singerdmx/flutter-quill/issues/1509)
2. Provide more support to all the platforms
### Bugs
Empty for now.
Please go to the [issues](https://github.com/singerdmx/flutter-quill/issues)
## Flutter Quill Extensions
### Features
1. Add support for cropping an image, fore more [info](https://github.com/singerdmx/flutter-quill/issues/1494)
2. Add support for copying images to the Clipboard
### Improvemenets
Please check the todos, this list will be updated soon.
### Bugs
Please check the todos, this list will be updated soon.

@ -1,6 +1,25 @@
[ [
{ {
"insert": "Here is an image: \n" "insert": "This is an asset 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 a network image: \n"
}, },
{ {
"insert": "\n" "insert": "\n"

@ -397,7 +397,7 @@ class _HomePageState extends State<HomePage> {
sizeSmall: TextStyle(fontSize: 9), sizeSmall: TextStyle(fontSize: 9),
), ),
embedBuilders: [ embedBuilders: [
...FlutterQuillEmbeds.editorsWebBuilders(), ...FlutterQuillEmbeds.editorWebBuilders(),
TimeStampEmbedBuilderWidget() TimeStampEmbedBuilderWidget()
], ],
), ),
@ -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()
], ],

@ -42,7 +42,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
expands: false, expands: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
embedBuilders: kIsWeb embedBuilders: kIsWeb
? FlutterQuillEmbeds.editorsWebBuilders() ? FlutterQuillEmbeds.editorWebBuilders()
: FlutterQuillEmbeds.editorBuilders(), : FlutterQuillEmbeds.editorBuilders(),
scrollable: true, scrollable: true,
autoFocus: true, autoFocus: true,
@ -57,7 +57,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
autoFocus: true, autoFocus: true,
expands: false, expands: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
embedBuilders: FlutterQuillEmbeds.editorsWebBuilders(), embedBuilders: FlutterQuillEmbeds.editorWebBuilders(),
scrollable: true, scrollable: true,
), ),
scrollController: ScrollController(), scrollController: ScrollController(),

@ -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,15 @@
## 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
- Add new static method in `FlutterQuillEmbeds` which is `defaultEditorBuilders` for minimal configurations
- 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
- Fix typo in the function name `editorsWebBuilders`, now it's called `editorWebBuilders`
- 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
- Update `README.md`
## 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:

@ -12,9 +12,7 @@ import 'presentation/embeds/editor/webview.dart';
import 'presentation/embeds/toolbar/camera_button/camera_button.dart'; import 'presentation/embeds/toolbar/camera_button/camera_button.dart';
import 'presentation/embeds/toolbar/formula_button.dart'; import 'presentation/embeds/toolbar/formula_button.dart';
import 'presentation/embeds/toolbar/image_button/image_button.dart'; import 'presentation/embeds/toolbar/image_button/image_button.dart';
// TODO: Temporary
// ignore: unused_import
import 'presentation/embeds/toolbar/media_button/media_button.dart';
import 'presentation/embeds/toolbar/video_button/video_button.dart'; import 'presentation/embeds/toolbar/video_button/video_button.dart';
import 'presentation/models/config/editor/image/image.dart'; import 'presentation/models/config/editor/image/image.dart';
import 'presentation/models/config/editor/image/image_web.dart'; import 'presentation/models/config/editor/image/image_web.dart';
@ -47,8 +45,7 @@ export 'presentation/embeds/toolbar/utils/image_video_utils.dart';
export 'presentation/embeds/toolbar/video_button/video_button.dart'; export 'presentation/embeds/toolbar/video_button/video_button.dart';
export 'presentation/embeds/utils.dart'; export 'presentation/embeds/utils.dart';
export 'presentation/models/config/editor/image/image.dart'; export 'presentation/models/config/editor/image/image.dart';
// TODO: Temporary
// ignore: unused_import
export 'presentation/models/config/editor/image/image_web.dart'; export 'presentation/models/config/editor/image/image_web.dart';
export 'presentation/models/config/editor/video/video.dart'; export 'presentation/models/config/editor/video/video.dart';
export 'presentation/models/config/editor/video/video_web.dart'; export 'presentation/models/config/editor/video/video_web.dart';
@ -72,7 +69,7 @@ class FlutterQuillEmbeds {
/// ///
/// **Note:** This method is not intended for web usage. /// **Note:** This method is not intended for web usage.
/// For web-specific embeds, /// For web-specific embeds,
/// use [editorsWebBuilders]. /// use [editorWebBuilders].
/// ///
/// ///
/// The method returns a list of [EmbedBuilder] objects that can be used with /// The method returns a list of [EmbedBuilder] objects that can be used with
@ -127,7 +124,7 @@ class FlutterQuillEmbeds {
/// [QuillEditorWebVideoEmbedBuilder] is the embed builder for handling /// [QuillEditorWebVideoEmbedBuilder] is the embed builder for handling
/// videos iframe on the web. /// videos iframe on the web.
/// ///
static List<EmbedBuilder> editorsWebBuilders( static List<EmbedBuilder> editorWebBuilders(
{QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations = {QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations =
const QuillEditorWebImageEmbedConfigurations(), const QuillEditorWebImageEmbedConfigurations(),
QuillEditorWebVideoEmbedConfigurations? videoEmbedConfigurations = QuillEditorWebVideoEmbedConfigurations? videoEmbedConfigurations =
@ -150,6 +147,15 @@ class FlutterQuillEmbeds {
]; ];
} }
/// Returns a list of default embed builders for QuillEditor.
///
/// It will use [editorWebBuilders] for web and [editorBuilders] for others
///
/// It's not customizable with minimal configurations
static List<EmbedBuilder> defaultEditorBuilders() {
return kIsWeb ? editorWebBuilders() : editorBuilders();
}
/// Returns a list of embed button builders to customize the toolbar buttons. /// Returns a list of embed button builders to customize the toolbar buttons.
/// ///
/// If you don't want to show one of the buttons for soem reason, /// If you don't want to show one of the buttons for soem reason,

@ -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,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
),
);
}
}
if (imageSize == null) {
image = getImageWidgetByImageSource(
imageUrl,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
);
imageSize = OptionalSize((image as Image).width, image.height);
}
if (!readOnly &&
(base.isMobile(supportWeb: false) ||
configurations.forceUseMobileOptionMenuForImageClick)) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final copyOption = SimpleDialogItem(
icon: Icons.copy_all_outlined,
color: Colors.cyanAccent,
text: 'Copy'.i18n,
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;
}
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: [
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,
);
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
// and that is up to the developer
if (!base.isMobile(supportWeb: false) &&
configurations.forceUseMobileOptionMenuForImageClick) {
return _menuOptionsForReadonlyImage(
context: context,
imageUrl: imageUrl,
image: image,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
);
}
return image;
}
// We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage(
context: context,
imageUrl: imageUrl,
image: image,
imageProviderBuilder: configurations.imageProviderBuilder, imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
alignment: alignment,
height: height,
width: width,
);
// OptionalSize? imageSize;
// final style = node.style.attributes['style'];
// if (style != null) {
// final attrs = base.isMobile(supportWeb: false)
// ? 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;
// 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);
// }
final imageSaverService =
QuillSharedExtensionsConfigurations.get(context: context)
.imageSaverService;
return GestureDetector(
child: Builder(
builder: (context) {
if (margin != null) {
return Padding(
padding: EdgeInsets.all(margin),
child: image,
);
}
return image;
},
),
onTap: () => showDialog(
context: context,
builder: (context) {
return ImageOptionsMenu(
controller: controller,
configurations: configurations,
imageSource: imageSource,
imageSize: imageSize,
isReadOnly: readOnly,
imageSaverService: imageSaverService,
);
},
),
); );
} }
} }
Widget _menuOptionsForReadonlyImage({ (
required BuildContext context, OptionalSize imageSize,
required String imageUrl, double? margin,
required Widget image, Alignment alignment,
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, ) _getImageAttributes(
required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, Node node,
}) { ) {
return GestureDetector( var imageSize = const OptionalSize(null, null);
onTap: () { var imageAlignment = Alignment.center;
showDialog( double? imageMargin;
context: context,
builder: (_) { // Usually double value
final saveOption = SimpleDialogItem( final heightValue = double.tryParse(
icon: Icons.save, node.style.attributes[Attribute.height.key]?.value.toString() ?? '');
color: Colors.greenAccent, final widthValue = double.tryParse(
text: 'Save'.i18n, node.style.attributes[Attribute.width.key]?.value.toString() ?? '');
onPressed: () async {
imageUrl = appendFileExtensionToImageUrl(imageUrl); if (heightValue != null) {
final messenger = ScaffoldMessenger.of(context); imageSize = imageSize.copyWith(
Navigator.of(context).pop(); height: heightValue,
);
}
if (widthValue != null) {
imageSize = imageSize.copyWith(
width: widthValue,
);
}
final saveImageResult = await saveImage( final cssStyle = node.style.attributes['style'];
imageUrl: imageUrl,
context: context, if (cssStyle != null) {
); final attrs = base.isMobile(supportWeb: false)
final imageSavedSuccessfully = saveImageResult.isSuccess; ? 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);
}
messenger.clearSnackBars(); // It css value as string but we will try to support it anyway
if (!imageSavedSuccessfully) { // TODO: This could be improved much better
messenger.showSnackBar(SnackBar( final cssHeightValue = double.tryParse(
content: Text( (attrs[Attribute.height.key] ?? '').replaceFirst('px', ''));
'Error while saving image'.i18n, final cssWidthValue = double.tryParse(
))); (attrs[Attribute.width.key] ?? '').replaceFirst('px', ''));
return;
}
String message; if (cssHeightValue != null) {
switch (saveImageResult.method) { imageSize = imageSize.copyWith(height: cssHeightValue);
case SaveImageResultMethod.network: }
message = 'Saved using the network'.i18n; if (cssWidthValue != null) {
break; imageSize = imageSize.copyWith(width: cssWidthValue);
case SaveImageResultMethod.localStorage: }
message = 'Saved using the local storage'.i18n;
break;
}
messenger.showSnackBar( imageAlignment = base.getAlignment(base.isMobile(supportWeb: false)
SnackBar( ? attrs[Attribute.mobileAlignment]
content: Text(message), : attrs[Attribute.alignment]);
), final margin = (base.isMobile(supportWeb: false)
); ? double.tryParse(Attribute.mobileMargin)
}, : double.tryParse(Attribute.margin));
); if (margin != null) {
final zoomOption = SimpleDialogItem( imageMargin = margin;
icon: Icons.zoom_in, }
color: Colors.cyanAccent, }
text: 'Zoom'.i18n,
onPressed: () { return (imageSize, imageMargin, imageAlignment);
Navigator.pushReplacement( }
context,
MaterialPageRoute( @immutable
builder: (context) => ImageTapWrapper( class OptionalSize {
imageUrl: imageUrl, const OptionalSize(
imageProviderBuilder: imageProviderBuilder, this.width,
imageErrorWidgetBuilder: imageErrorWidgetBuilder, this.height,
),
),
);
},
);
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';

@ -1,8 +1,8 @@
import 'dart:io' show File; 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 '../../logic/services/image_saver/s_image_saver.dart';
import '../../logic/models/config/shared_configurations.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 +49,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,7 +60,7 @@ 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,

@ -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(
return Image.memory( image: getImageProviderByImageSource(
base64.decode(imageSource),
width: width,
height: height,
alignment: alignment,
);
}
if (imageProviderBuilder != null) {
return Image(
image: imageProviderBuilder(imageSource),
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) {
@ -173,8 +160,11 @@ class ImageTapWrapper extends StatelessWidget {
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: child: Icon(
Icon(Icons.close, color: Colors.grey[400], size: 28), Icons.close,
color: Colors.grey[400],
size: 28,
),
) )
], ],
), ),

@ -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,
@ -161,6 +162,7 @@ class QuillEditorImageEmbedConfigurations {
imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder, imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder,
imageErrorWidgetBuilder: imageErrorWidgetBuilder:
imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder, imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder,
// ignore: deprecated_member_use_from_same_package
forceUseMobileOptionMenuForImageClick: forceUseMobileOptionMenuForImageClick:
forceUseMobileOptionMenuForImageClick ?? forceUseMobileOptionMenuForImageClick ??
this.forceUseMobileOptionMenuForImageClick, this.forceUseMobileOptionMenuForImageClick,

@ -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,
),
); );
} }
} }
@ -167,12 +175,13 @@ class _TextLineState extends State<TextLine> {
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
); );
return RichTextProxy( return RichTextProxy(
textStyle: textSpan.style!, textStyle: textSpan.style!,
textAlign: textAlign, textAlign: textAlign,
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