First step of improving flutter quill extensions

pull/1564/head
Ellet 1 year ago
parent 7ccd61e63e
commit 0d88c0201a
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 3
      .gitignore
  2. 4
      example/lib/presentation/quill/quill_editor.dart
  3. 6
      flutter_quill_extensions/CHANGELOG.md
  4. 0
      flutter_quill_extensions/lib/embeds/embed_types.dart
  5. 0
      flutter_quill_extensions/lib/embeds/formula/editor/formula_embed.dart
  6. 2
      flutter_quill_extensions/lib/embeds/formula/toolbar/formula_button.dart
  7. 86
      flutter_quill_extensions/lib/embeds/image/editor/image_embed.dart
  8. 4
      flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart
  9. 7
      flutter_quill_extensions/lib/embeds/image/editor/image_menu.dart
  10. 6
      flutter_quill_extensions/lib/embeds/image/editor/image_web_embed.dart
  11. 8
      flutter_quill_extensions/lib/embeds/image/toolbar/image_button.dart
  12. 2
      flutter_quill_extensions/lib/embeds/image/toolbar/select_image_source.dart
  13. 6
      flutter_quill_extensions/lib/embeds/others/camera_button/camera_button.dart
  14. 4
      flutter_quill_extensions/lib/embeds/others/camera_button/camera_types.dart
  15. 2
      flutter_quill_extensions/lib/embeds/others/camera_button/select_camera_action.dart
  16. 0
      flutter_quill_extensions/lib/embeds/others/image_video_utils.dart
  17. 0
      flutter_quill_extensions/lib/embeds/others/media_button/media_button.dart
  18. 0
      flutter_quill_extensions/lib/embeds/unknown/editor/unknown_embed.dart
  19. 1
      flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart
  20. 6
      flutter_quill_extensions/lib/embeds/video/editor/video_web_embed.dart
  21. 2
      flutter_quill_extensions/lib/embeds/video/toolbar/select_video_source.dart
  22. 8
      flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart
  23. 4
      flutter_quill_extensions/lib/embeds/video/video.dart
  24. 2
      flutter_quill_extensions/lib/embeds/widgets/image.dart
  25. 0
      flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart
  26. 2
      flutter_quill_extensions/lib/embeds/widgets/video_app.dart
  27. 0
      flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart
  28. 0
      flutter_quill_extensions/lib/extensions/attribute.dart
  29. 23
      flutter_quill_extensions/lib/extensions/controller.dart
  30. 87
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  31. 2
      flutter_quill_extensions/lib/models/config/editor/image/image.dart
  32. 0
      flutter_quill_extensions/lib/models/config/editor/image/image_web.dart
  33. 0
      flutter_quill_extensions/lib/models/config/editor/video/video.dart
  34. 0
      flutter_quill_extensions/lib/models/config/editor/video/video_web.dart
  35. 0
      flutter_quill_extensions/lib/models/config/editor/webview.dart
  36. 0
      flutter_quill_extensions/lib/models/config/shared_configurations.dart
  37. 2
      flutter_quill_extensions/lib/models/config/toolbar/buttons/camera.dart
  38. 0
      flutter_quill_extensions/lib/models/config/toolbar/buttons/formula.dart
  39. 2
      flutter_quill_extensions/lib/models/config/toolbar/buttons/image.dart
  40. 0
      flutter_quill_extensions/lib/models/config/toolbar/buttons/media_button.dart
  41. 2
      flutter_quill_extensions/lib/models/config/toolbar/buttons/video.dart
  42. 150
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart
  43. 58
      flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart
  44. 201
      flutter_quill_extensions/lib/presentation/utils/utils.dart
  45. 0
      flutter_quill_extensions/lib/services/image_picker/image_options.dart
  46. 0
      flutter_quill_extensions/lib/services/image_picker/image_picker.dart
  47. 0
      flutter_quill_extensions/lib/services/image_picker/packages/image_picker.dart
  48. 0
      flutter_quill_extensions/lib/services/image_picker/s_image_picker.dart
  49. 0
      flutter_quill_extensions/lib/services/image_saver/exceptions.dart
  50. 0
      flutter_quill_extensions/lib/services/image_saver/image_saver.dart
  51. 0
      flutter_quill_extensions/lib/services/image_saver/packages/gal.dart
  52. 0
      flutter_quill_extensions/lib/services/image_saver/s_image_saver.dart
  53. 0
      flutter_quill_extensions/lib/utils/dart_ui/dart_ui_fake.dart
  54. 0
      flutter_quill_extensions/lib/utils/dart_ui/dart_ui_real.dart
  55. 15
      flutter_quill_extensions/lib/utils/element_utils/element_shared_utils.dart
  56. 108
      flutter_quill_extensions/lib/utils/element_utils/element_utils.dart
  57. 10
      flutter_quill_extensions/lib/utils/element_utils/element_web_utils.dart
  58. 2
      flutter_quill_extensions/lib/utils/quill_image_utils.dart
  59. 0
      flutter_quill_extensions/lib/utils/string.dart
  60. 89
      flutter_quill_extensions/lib/utils/utils.dart
  61. 2
      flutter_quill_extensions/pubspec.yaml

3
.gitignore vendored

@ -81,3 +81,6 @@ pubspec.lock
pubspec_overrides.yaml
old_example
# A directory where you put all of your local things that you don't want to push
.flutter-quill

@ -7,9 +7,9 @@ import 'package:desktop_drop/desktop_drop.dart' show DropTarget;
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:flutter_quill_extensions/presentation/embeds/widgets/image.dart'
import 'package:flutter_quill_extensions/embeds/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:path/path.dart' as path;
import '../extensions/scaffold_messenger.dart';

@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## 0.8.0
* **Breaking Change**: Completly change the way how the source code structured to more basic and simple way, organize folders and file names, if you use the library
from `flutter_quill_extensions.dart` then there is nothing you need to do, but if you are using any other import then you need to re-imports
* Add support for unoffical css property called `deletable` in the image embed, this won't affect how quill js work
* Improvemenets to the image embed
## 0.7.2
* Fix a bug when opening the link dialog for both video and image buttons
* Update `README.md`

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import '../../models/config/toolbar/buttons/formula.dart';
import '../../../models/config/toolbar/buttons/formula.dart';
class QuillToolbarFormulaButton extends StatelessWidget {
const QuillToolbarFormulaButton({

@ -0,0 +1,86 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize;
import '../../../models/config/editor/image/image.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../widgets/image.dart';
import 'image_menu.dart';
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
QuillEditorImageEmbedBuilder({
required this.configurations,
});
final QuillEditorImageEmbedConfigurations configurations;
@override
String get key => BlockEmbed.imageType;
@override
bool get expanded => false;
@override
Widget build(
BuildContext context,
QuillController controller,
base.Embed node,
bool readOnly,
bool inline,
TextStyle textStyle,
) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
final imageSource = standardizeImageUrl(node.value.data);
final ((imageSize), margin, alignment) = getElementAttributes(node);
final width = imageSize.width;
final height = imageSize.height;
final image = getImageWidgetByImageSource(
imageSource,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
alignment: alignment,
height: height,
width: width,
assetsPrefix: QuillSharedExtensionsConfigurations.get(context: context)
.assetsPrefix,
);
final imageSaverService =
QuillSharedExtensionsConfigurations.get(context: context)
.imageSaverService;
return GestureDetector(
onTap: configurations.onImageClicked ??
() => showDialog(
context: context,
builder: (_) => QuillProvider.value(
value: context.requireQuillProvider,
child: FlutterQuillLocalizationsWidget(
child: ImageOptionsMenu(
controller: controller,
configurations: configurations,
imageSource: imageSource,
imageSize: imageSize,
isReadOnly: readOnly,
imageSaverService: imageSaverService,
),
),
),
),
child: Builder(
builder: (context) {
if (margin != null) {
return Padding(
padding: EdgeInsets.all(margin),
child: image,
);
}
return image;
},
),
);
}
}

@ -4,8 +4,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../logic/extensions/controller.dart';
import '../../../logic/services/image_picker/s_image_picker.dart';
import '../../../extensions/controller.dart';
import '../../../services/image_picker/s_image_picker.dart';
/// When request picking an image, for example when the image button toolbar
/// clicked, it should be null in case the user didn't choose any image or

@ -12,10 +12,11 @@ import 'package:flutter_quill/flutter_quill.dart'
getEmbedNode;
import 'package:flutter_quill/translations.dart';
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../../logic/services/image_saver/s_image_saver.dart';
import '../../../../logic/utils/string.dart';
import '../../../models/config/editor/image/image.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../services/image_saver/s_image_saver.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../../utils/string.dart';
import '../../../utils/utils.dart';
import '../../widgets/image.dart' show ImageTapWrapper, getImageStyleString;
import '../../widgets/image_resizer.dart' show ImageResizer;

@ -4,10 +4,10 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:universal_html/html.dart' as html;
import '../../../models/config/editor/image/image_web.dart';
import '../../../utils/dart_ui/dart_ui_fake.dart'
if (dart.library.html) '../../../utils/dart_ui/dart_ui_real.dart' as ui;
import '../../../utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart';
import '../../../utils/web_utils.dart';
import '../shims/dart_ui_fake.dart'
if (dart.library.html) '../shims/dart_ui_real.dart' as ui;
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
const QuillEditorWebImageEmbedBuilder({

@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart';
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../../logic/services/image_picker/image_picker.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../models/config/toolbar/buttons/image.dart';
import '../../embed_types/image.dart';
import '../utils/image_video_utils.dart';
import '../../../services/image_picker/image_picker.dart';
import '../../others/image_video_utils.dart';
import '../editor/image_embed_types.dart';
import 'select_image_source.dart';
class QuillToolbarImageButton extends StatelessWidget {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop;
import '../../embed_types/image.dart';
import '../editor/image_embed_types.dart';
class SelectImageSourceDialog extends StatelessWidget {
const SelectImageSourceDialog({super.key});

@ -8,10 +8,10 @@ import 'package:flutter_quill/flutter_quill.dart'
QuillToolbarIconButton;
import 'package:flutter_quill/translations.dart';
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../../logic/services/image_picker/image_options.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../models/config/toolbar/buttons/camera.dart';
import '../../embed_types/camera.dart';
import '../../../services/image_picker/image_options.dart';
import 'camera_types.dart';
import 'select_camera_action.dart';
class QuillToolbarCameraButton extends StatelessWidget {

@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart' show BuildContext;
import 'package:meta/meta.dart' show immutable;
import 'image.dart';
import 'video.dart';
import '../../image/editor/image_embed_types.dart';
import '../../video/video.dart';
enum CameraAction {
video,

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/translations.dart';
import '../../embed_types/camera.dart';
import 'camera_types.dart';
class SelectCameraActionDialog extends StatelessWidget {
const SelectCameraActionDialog({super.key});

@ -4,6 +4,7 @@ import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart';
import '../../../models/config/editor/video/video.dart';
import '../../../utils/element_utils/element_utils.dart';
import '../../../utils/utils.dart';
import '../../widgets/video_app.dart';
import '../../widgets/youtube_video_app.dart';

@ -5,10 +5,10 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart'
show YoutubePlayer;
import '../../../models/config/editor/video/video_web.dart';
import '../../../utils/dart_ui/dart_ui_fake.dart'
if (dart.library.html) '../../../utils/dart_ui/dart_ui_real.dart' as ui;
import '../../../utils/element_utils/element_web_utils.dart';
import '../../../utils/utils.dart';
import '../../../utils/web_utils.dart';
import '../shims/dart_ui_fake.dart'
if (dart.library.html) '../shims/dart_ui_real.dart' as ui;
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
const QuillEditorWebVideoEmbedBuilder({

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isDesktop;
import '../../embed_types/video.dart';
import '../video.dart';
class SelectVideoSourceDialog extends StatelessWidget {
const SelectVideoSourceDialog({super.key});

@ -3,11 +3,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../../logic/services/image_picker/image_options.dart';
import '../../../models/config/shared_configurations.dart';
import '../../../models/config/toolbar/buttons/video.dart';
import '../../embed_types/video.dart';
import '../utils/image_video_utils.dart';
import '../../../services/image_picker/image_options.dart';
import '../../others/image_video_utils.dart';
import '../video.dart';
import 'select_video_source.dart';
class QuillToolbarVideoButton extends StatelessWidget {

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../logic/extensions/controller.dart';
import '../../../logic/services/image_picker/s_image_picker.dart';
import '../../extensions/controller.dart';
import '../../services/image_picker/s_image_picker.dart';
/// When request picking an video, for example when the video button toolbar
/// clicked, it should be null in case the user didn't choose any video or

@ -7,7 +7,7 @@ import 'package:photo_view/photo_view.dart';
import '../../models/config/editor/image/image.dart';
import '../../utils/utils.dart';
import '../embed_types/image.dart';
import '../image/editor/image_embed_types.dart';
const List<String> imageFileExtensions = [
'.jpeg',

@ -6,7 +6,7 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart';
import '../../../flutter_quill_extensions.dart';
import '../../flutter_quill_extensions.dart';
/// Widget for playing back video
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player

@ -1,7 +1,5 @@
import 'package:flutter_quill/flutter_quill.dart';
// ignore: unused_import
import '../../presentation/embeds/editor/webview.dart';
import '../utils/quill_image_utils.dart';
/// Extension functions on [QuillController]
@ -12,27 +10,6 @@ extension QuillControllerExt on QuillController {
int get index => selection.baseOffset;
int get length => selection.extentOffset - index;
/// Insert webview embed block, it requires [initialUrl] to load
/// the initial page
// void insertWebViewBlock({
// required String initialUrl,
// }) {
// final block = BlockEmbed.custom(
// QuillEditorWebViewBlockEmbed(
// initialUrl,
// ),
// );
// this
// ..skipRequestKeyboard = true
// ..replaceText(
// index,
// length,
// block,
// null,
// );
// }
/// Insert image embed block, it requires the [imageSource]
///
/// it could be local image on the system file

@ -6,52 +6,49 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import 'presentation/embeds/editor/image/image.dart';
import 'presentation/embeds/editor/image/image_web.dart';
import 'presentation/embeds/editor/video/video.dart';
import 'presentation/embeds/editor/video/video_web.dart';
import 'presentation/embeds/editor/webview.dart';
import 'presentation/embeds/toolbar/camera_button/camera_button.dart';
import 'presentation/embeds/toolbar/image_button/image_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_web.dart';
import 'presentation/models/config/editor/video/video.dart';
import 'presentation/models/config/editor/video/video_web.dart';
import 'presentation/models/config/editor/webview.dart';
import 'presentation/models/config/toolbar/buttons/camera.dart';
import 'presentation/models/config/toolbar/buttons/image.dart';
import 'presentation/models/config/toolbar/buttons/media_button.dart';
import 'presentation/models/config/toolbar/buttons/video.dart';
import 'embeds/image/editor/image_embed.dart';
import 'embeds/image/editor/image_web_embed.dart';
import 'embeds/image/toolbar/image_button.dart';
import 'embeds/others/camera_button/camera_button.dart';
import 'embeds/video/editor/video_embed.dart';
import 'embeds/video/editor/video_web_embed.dart';
import 'embeds/video/toolbar/video_button.dart';
import 'models/config/editor/image/image.dart';
import 'models/config/editor/image/image_web.dart';
import 'models/config/editor/video/video.dart';
import 'models/config/editor/video/video_web.dart';
import 'models/config/editor/webview.dart';
import 'models/config/toolbar/buttons/camera.dart';
import 'models/config/toolbar/buttons/image.dart';
import 'models/config/toolbar/buttons/media_button.dart';
import 'models/config/toolbar/buttons/video.dart';
export '/logic/extensions/controller.dart';
export '/presentation/models/config/editor/webview.dart';
export 'logic/models/config/shared_configurations.dart';
export 'presentation/embeds/editor/image/image.dart';
export 'presentation/embeds/editor/image/image_web.dart';
export 'presentation/embeds/editor/unknown.dart';
export 'presentation/embeds/editor/video/video.dart';
export 'presentation/embeds/editor/video/video_web.dart';
export 'presentation/embeds/editor/webview.dart';
export 'presentation/embeds/embed_types.dart';
export 'presentation/embeds/embed_types/image.dart';
export 'presentation/embeds/embed_types/video.dart';
export 'presentation/embeds/toolbar/camera_button/camera_button.dart';
export 'presentation/embeds/toolbar/formula_button.dart';
export 'presentation/embeds/toolbar/image_button/image_button.dart';
export 'presentation/embeds/toolbar/media_button/media_button.dart';
export 'presentation/embeds/toolbar/utils/image_video_utils.dart';
export 'presentation/embeds/toolbar/video_button/video_button.dart';
export 'presentation/models/config/editor/image/image.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_web.dart';
export 'presentation/models/config/toolbar/buttons/camera.dart';
export 'presentation/models/config/toolbar/buttons/formula.dart';
export 'presentation/models/config/toolbar/buttons/image.dart';
export 'presentation/models/config/toolbar/buttons/media_button.dart';
export 'presentation/models/config/toolbar/buttons/video.dart';
export 'presentation/utils/utils.dart';
export 'embeds/embed_types.dart';
export 'embeds/formula/toolbar/formula_button.dart';
export 'embeds/image/editor/image_embed.dart';
export 'embeds/image/editor/image_embed_types.dart';
export 'embeds/image/editor/image_web_embed.dart';
export 'embeds/image/toolbar/image_button.dart';
export 'embeds/others/camera_button/camera_button.dart';
export 'embeds/others/media_button/media_button.dart';
export 'embeds/unknown/editor/unknown_embed.dart';
export 'embeds/video/editor/video_embed.dart';
export 'embeds/video/editor/video_web_embed.dart';
export 'embeds/video/toolbar/video_button.dart';
export 'embeds/video/video.dart';
export 'extensions/controller.dart';
export 'models/config/editor/image/image.dart';
export 'models/config/editor/image/image_web.dart';
export 'models/config/editor/video/video.dart';
export 'models/config/editor/video/video_web.dart';
export 'models/config/editor/webview.dart';
export 'models/config/shared_configurations.dart';
export 'models/config/toolbar/buttons/camera.dart';
export 'models/config/toolbar/buttons/formula.dart';
export 'models/config/toolbar/buttons/image.dart';
export 'models/config/toolbar/buttons/media_button.dart';
export 'models/config/toolbar/buttons/video.dart';
export 'utils/utils.dart';
@immutable
class FlutterQuillEmbeds {

@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart' show VoidCallback;
import 'package:flutter_quill/extensions.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/embed_types/image.dart';
import '../../../../embeds/image/editor/image_embed_types.dart';
/// [QuillEditorImageEmbedConfigurations] for desktop, mobile and
/// other platforms

@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_quill/flutter_quill.dart';
import '../../../../embeds/embed_types/camera.dart';
import '../../../../embeds/others/camera_button/camera_types.dart';
class QuillToolbarCameraButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../embeds/embed_types/image.dart';
import '../../../../embeds/image/editor/image_embed_types.dart';
class QuillToolbarImageButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_quill/flutter_quill.dart';
import '../../../../embeds/embed_types/video.dart';
import '../../../../embeds/video/video.dart';
class QuillToolbarVideoButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions {

@ -1,150 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize;
import '../../../../logic/models/config/shared_configurations.dart';
import '../../../models/config/editor/image/image.dart';
import '../../../utils/utils.dart';
import '../../widgets/image.dart';
import 'image_menu.dart';
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
QuillEditorImageEmbedBuilder({
required this.configurations,
});
final QuillEditorImageEmbedConfigurations configurations;
@override
String get key => BlockEmbed.imageType;
@override
bool get expanded => false;
@override
Widget build(
BuildContext context,
QuillController controller,
base.Embed node,
bool readOnly,
bool inline,
TextStyle textStyle,
) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
final imageSource = standardizeImageUrl(node.value.data);
final ((imageSize), margin, alignment) = getElementAttributes(node);
final width = imageSize.width;
final height = imageSize.height;
final image = getImageWidgetByImageSource(
imageSource,
imageProviderBuilder: configurations.imageProviderBuilder,
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder,
alignment: alignment,
height: height,
width: width,
assetsPrefix: QuillSharedExtensionsConfigurations.get(context: context)
.assetsPrefix,
);
// 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(
onTap: configurations.onImageClicked ??
() => showDialog(
context: context,
builder: (_) {
return QuillProvider.value(
value: context.requireQuillProvider,
child: FlutterQuillLocalizationsWidget(
child: ImageOptionsMenu(
controller: controller,
configurations: configurations,
imageSource: imageSource,
imageSize: imageSize,
isReadOnly: readOnly,
imageSaverService: imageSaverService,
),
),
);
},
),
child: Builder(
builder: (context) {
if (margin != null) {
return Padding(
padding: EdgeInsets.all(margin),
child: image,
);
}
return image;
},
),
);
}
}

@ -1,58 +0,0 @@
// import 'dart:convert' show jsonDecode, jsonEncode;
// import 'package:flutter/widgets.dart';
// import 'package:flutter_inappwebview/flutter_inappwebview.dart';
// import 'package:flutter_quill/flutter_quill.dart';
// import 'package:meta/meta.dart' show experimental;
// import '../../models/config/editor/webview.dart';
// @experimental
// class QuillEditorWebViewBlockEmbed extends CustomBlockEmbed {
// const QuillEditorWebViewBlockEmbed(
// String value,
// ) : super(webViewType, value);
// factory QuillEditorWebViewBlockEmbed.fromDocument(Document document) =>
// QuillEditorWebViewBlockEmbed(jsonEncode(document.toDelta().toJson()));
// static const String webViewType = 'webview';
// Document get document => Document.fromJson(jsonDecode(data));
// }
// @experimental
// class QuillEditorWebViewEmbedBuilder extends EmbedBuilder {
// const QuillEditorWebViewEmbedBuilder({
// required this.configurations,
// });
// @override
// bool get expanded => false;
// final QuillEditorWebViewEmbedConfigurations configurations;
// @override
// Widget build(
// BuildContext context,
// QuillController controller,
// Embed node,
// bool readOnly,
// bool inline,
// TextStyle textStyle,
// ) {
// final url = node.value.data as String;
// return SizedBox(
// width: double.infinity,
// height: 200,
// child: InAppWebView(
// initialUrlRequest: URLRequest(
// url: Uri.parse(url),
// ),
// ),
// );
// }
// @override
// String get key => 'webview';
// }

@ -1,201 +0,0 @@
import 'dart:io' show File;
import 'package:flutter/foundation.dart' show immutable;
import 'package:flutter/widgets.dart' show Alignment;
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
import '../../logic/extensions/attribute.dart';
import '../../logic/services/image_saver/s_image_saver.dart';
import '../embeds/widgets/image.dart';
RegExp _base64 = RegExp(
r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
);
bool isBase64(String str) {
return _base64.hasMatch(str);
}
bool isHttpBasedUrl(String url) {
try {
final uri = Uri.parse(url.trim());
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
} catch (_) {
return false;
}
}
bool isImageBase64(String imageUrl) {
return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl);
}
bool isYouTubeUrl(String videoUrl) {
try {
final uri = Uri.parse(videoUrl);
return uri.host == 'www.youtube.com' ||
uri.host == 'youtube.com' ||
uri.host == 'youtu.be' ||
uri.host == 'www.youtu.be';
} catch (_) {
return false;
}
}
enum SaveImageResultMethod { network, localStorage }
@immutable
class SaveImageResult {
const SaveImageResult({required this.error, required this.method});
final String? error;
final SaveImageResultMethod method;
}
Future<SaveImageResult> saveImage({
required String imageUrl,
required ImageSaverService imageSaverService,
}) async {
final imageFile = File(imageUrl);
final hasPermission = await imageSaverService.hasAccess();
if (!hasPermission) {
await imageSaverService.requestAccess();
}
final imageExistsLocally = await imageFile.exists();
if (!imageExistsLocally) {
try {
await imageSaverService.saveImageFromNetwork(
Uri.parse(appendFileExtensionToImageUrl(imageUrl)),
);
return const SaveImageResult(
error: null,
method: SaveImageResultMethod.network,
);
} catch (e) {
return SaveImageResult(
error: e.toString(),
method: SaveImageResultMethod.network,
);
}
}
try {
await imageSaverService.saveLocalImage(imageUrl);
return const SaveImageResult(
error: null,
method: SaveImageResultMethod.localStorage,
);
} catch (e) {
return SaveImageResult(
error: e.toString(),
method: SaveImageResultMethod.localStorage,
);
}
}
(
OptionalSize elementSize,
double? margin,
Alignment alignment,
) getElementAttributes(
Node node,
) {
var elementSize = const OptionalSize(null, null);
var elementAlignment = Alignment.center;
double? elementMargin;
// 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) {
elementSize = elementSize.copyWith(
height: heightValue,
);
}
if (widthValue != null) {
elementSize = elementSize.copyWith(
width: widthValue,
);
}
final cssStyle = node.style.attributes['style'];
if (cssStyle != null) {
final attrs = base.isMobile(supportWeb: false)
? base.parseKeyValuePairs(cssStyle.value.toString(), {
AttributeExt.mobileWidth.key,
AttributeExt.mobileHeight.key,
AttributeExt.mobileMargin.key,
AttributeExt.mobileAlignment.key,
})
: base.parseKeyValuePairs(cssStyle.value.toString(), {
Attribute.width.key,
Attribute.height.key,
'margin',
'alignment',
});
if (attrs.isEmpty) {
return (elementSize, elementMargin, elementAlignment);
}
// It css value as string but we will try to support it anyway
// TODO: This could be improved much better
final cssHeightValue = double.tryParse(((base.isMobile(supportWeb: false)
? attrs[AttributeExt.mobileHeight.key]
: attrs[Attribute.height.key]) ??
'')
.replaceFirst('px', ''));
final cssWidthValue = double.tryParse(((!base.isMobile(supportWeb: false)
? attrs[Attribute.width.key]
: attrs[AttributeExt.mobileWidth.key]) ??
'')
.replaceFirst('px', ''));
if (cssHeightValue != null) {
elementSize = elementSize.copyWith(height: cssHeightValue);
}
if (cssWidthValue != null) {
elementSize = elementSize.copyWith(width: cssWidthValue);
}
elementAlignment = base.getAlignment(base.isMobile(supportWeb: false)
? attrs[AttributeExt.mobileAlignment.key]
: attrs['alignment']);
final margin = (base.isMobile(supportWeb: false)
? double.tryParse(AttributeExt.mobileMargin.key)
: double.tryParse('margin'));
if (margin != null) {
elementMargin = margin;
}
}
return (elementSize, elementMargin, elementAlignment);
}
@immutable
class OptionalSize {
const OptionalSize(
this.width,
this.height,
);
/// 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,15 @@
Map<String, String> parseCssString(String cssString) {
final result = <String, String>{};
final declarations = cssString.split(';');
for (final declaration in declarations) {
final parts = declaration.split(':');
if (parts.length == 2) {
final property = parts[0].trim();
final value = parts[1].trim();
result[property] = value;
}
}
return result;
}

@ -0,0 +1,108 @@
import 'package:flutter/foundation.dart' show immutable;
import 'package:flutter/widgets.dart' show Alignment;
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
import '../../extensions/attribute.dart';
import 'element_shared_utils.dart';
/// Theses properties are not officaly supported by quill js
/// but they are only used in all platforms other than web
/// and they will be stored in css style property so quill js ignore them
enum ExtraElementProperties {
deletable,
}
(
OptionalSize elementSize,
double? margin,
Alignment alignment,
) getElementAttributes(
Node node,
) {
var elementSize = const OptionalSize(null, null);
var elementAlignment = Alignment.center;
double? elementMargin;
// 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) {
elementSize = elementSize.copyWith(
height: heightValue,
);
}
if (widthValue != null) {
elementSize = elementSize.copyWith(
width: widthValue,
);
}
final cssStyle = node.style.attributes['style'];
if (cssStyle != null) {
// It css value as string but we will try to support it anyway
final cssAttrs = parseCssString(cssStyle.value.toString());
// TODO: This could be improved much better
final cssHeightValue = double.tryParse(((base.isMobile(supportWeb: false)
? cssAttrs[AttributeExt.mobileHeight.key]
: cssAttrs[Attribute.height.key]) ??
'')
.replaceFirst('px', ''));
final cssWidthValue = double.tryParse(((!base.isMobile(supportWeb: false)
? cssAttrs[Attribute.width.key]
: cssAttrs[AttributeExt.mobileWidth.key]) ??
'')
.replaceFirst('px', ''));
if (cssHeightValue != null) {
elementSize = elementSize.copyWith(height: cssHeightValue);
}
if (cssWidthValue != null) {
elementSize = elementSize.copyWith(width: cssWidthValue);
}
elementAlignment = base.getAlignment(base.isMobile(supportWeb: false)
? cssAttrs[AttributeExt.mobileAlignment.key]
: cssAttrs['alignment']);
final margin = (base.isMobile(supportWeb: false)
? double.tryParse(AttributeExt.mobileMargin.key)
: double.tryParse('margin'));
if (margin != null) {
elementMargin = margin;
}
}
return (elementSize, elementMargin, elementAlignment);
}
@immutable
class OptionalSize {
const OptionalSize(
this.width,
this.height,
);
/// 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,
);
}
}

@ -1,6 +1,7 @@
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
import 'element_shared_utils.dart';
/// Prefer the width, and height from the css style attribute if exits
/// it can be `auto` or `100px` so it's specific to HTML && CSS
/// if not, we will use the one from attributes which is usually just an double
@ -27,12 +28,7 @@ import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
final widthValue = node.style.attributes[Attribute.width.key]?.value;
if (cssStyle != null) {
final attrs = base.parseKeyValuePairs(cssStyle.value.toString(), {
Attribute.width.key,
Attribute.height.key,
'margin',
'alignment',
});
final attrs = parseCssString(cssStyle.value.toString());
final cssHeightValue = attrs[Attribute.height.key];
if (cssHeightValue != null) {
height = cssHeightValue;

@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_quill/flutter_quill.dart' as quill;
import 'package:path/path.dart' as path;
import '../../presentation/utils/utils.dart';
import 'utils.dart';
typedef OnGenerateNewFileNameCallback = String Function(
String currentFileName,

@ -0,0 +1,89 @@
import 'dart:io' show File;
import 'package:flutter/foundation.dart' show immutable;
import '../embeds/widgets/image.dart';
import '../services/image_saver/s_image_saver.dart';
RegExp _base64 = RegExp(
r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
);
bool isBase64(String str) {
return _base64.hasMatch(str);
}
bool isHttpBasedUrl(String url) {
try {
final uri = Uri.parse(url.trim());
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
} catch (_) {
return false;
}
}
bool isImageBase64(String imageUrl) {
return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl);
}
bool isYouTubeUrl(String videoUrl) {
try {
final uri = Uri.parse(videoUrl);
return uri.host == 'www.youtube.com' ||
uri.host == 'youtube.com' ||
uri.host == 'youtu.be' ||
uri.host == 'www.youtu.be';
} catch (_) {
return false;
}
}
enum SaveImageResultMethod { network, localStorage }
@immutable
class SaveImageResult {
const SaveImageResult({required this.error, required this.method});
final String? error;
final SaveImageResultMethod method;
}
Future<SaveImageResult> saveImage({
required String imageUrl,
required ImageSaverService imageSaverService,
}) async {
final imageFile = File(imageUrl);
final hasPermission = await imageSaverService.hasAccess();
if (!hasPermission) {
await imageSaverService.requestAccess();
}
final imageExistsLocally = await imageFile.exists();
if (!imageExistsLocally) {
try {
await imageSaverService.saveImageFromNetwork(
Uri.parse(appendFileExtensionToImageUrl(imageUrl)),
);
return const SaveImageResult(
error: null,
method: SaveImageResultMethod.network,
);
} catch (e) {
return SaveImageResult(
error: e.toString(),
method: SaveImageResultMethod.network,
);
}
}
try {
await imageSaverService.saveLocalImage(imageUrl);
return const SaveImageResult(
error: null,
method: SaveImageResultMethod.localStorage,
);
} catch (e) {
return SaveImageResult(
error: e.toString(),
method: SaveImageResultMethod.localStorage,
);
}
}

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

Loading…
Cancel
Save