From d83cc0cf4cadbe7bdeae090ae6cf5352a5928b31 Mon Sep 17 00:00:00 2001 From: BambinoUA <130981115+MacDeveloper1@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:28:19 +0200 Subject: [PATCH] Image embedding tweaks (#1187) * Update `QuillDialogTheme` * Add `ImageAttribute` and `VideoAttribute` * Add `LinkStyleDialog` * Update `RawEditor` with new actions * Update translations * Add translations * Restore original `LinkStyleButton` * Update dart SDK to 2.17.0 * Update `ScriptAttribute` and `ScriptAttributes` * Add `LinkStyleButton2` * Implement `ApplyLinkAction` for `Ctrl+K` * Use `ShortcutActivator` instead of `LogicalKeySet` * Use `ShortcutActivator` instead of `LogicalKeySet` * Update `QuillDialogTheme` with `buttonStyle` * Implement `dialogTheme` for `QuillEditor` * Update `QuillDialogTheme` with dialog constraints * Pass `dialogTheme` to `RawEditor` * Merge `customShorcuts` and `customActions` * Implement `constrains` and `buttonStyle` * Update `QuillDialogTheme` with `isWrappable` * Update `LinkStyleDialog` to use `Wrap` conditionally * Add `ImageButton2` * Update `QuillDialogTheme` with padding properties * Update `QuillDialogTheme` * Export `UtilityWidgets` via flutter_quill.extensions * Minor change * Minor change * Update `QuillDialogTheme` * Update translations * Add `MediaButton` * Add `ImageEmbedBuilderWeb` * Update `flutter_quill`' version * Update `flutter_quill_extensions` * Update CHANGELOG.md --- CHANGELOG.md | 8 +- .../lib/embeds/builders.dart | 38 ++ .../lib/embeds/embed_types.dart | 26 + .../lib/embeds/toolbar/media_button.dart | 452 ++++++++++++++++++ .../lib/embeds/widgets/image.dart | 2 +- .../lib/flutter_quill_extensions.dart | 118 ++--- .../lib/shims/dart_ui_fake.dart | 23 + .../lib/shims/dart_ui_real.dart | 1 + flutter_quill_extensions/pubspec.yaml | 13 +- lib/extensions.dart | 1 + lib/src/models/themes/quill_dialog_theme.dart | 32 +- lib/src/translations/toolbar.i18n.dart | 2 + .../widgets/toolbar/link_style_button2.dart | 1 - pubspec.yaml | 2 +- 14 files changed, 645 insertions(+), 74 deletions(-) create mode 100644 flutter_quill_extensions/lib/embeds/toolbar/media_button.dart create mode 100644 flutter_quill_extensions/lib/shims/dart_ui_fake.dart create mode 100644 flutter_quill_extensions/lib/shims/dart_ui_real.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f4f80e3..094eee38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# [7.1.10] +- Image embedding tweaks + - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. + - Implement image insert for web (image as base64) + # [7.1.9] - Editor tweaks PR from [bambinoua](https://github.com/bambinoua). @@ -8,14 +13,12 @@ - Update minimum Dart SDK version to 2.17.0 to use enum extensions. - Use merging shortcuts and actions correclty (if the key combination is the same) - # [7.1.8] - Dropdown tweaks - Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. - Remove alignment property as useless. - Fix bugs with max width when width property is null. - # [7.1.7] - Toolbar tweaks. @@ -30,7 +33,6 @@ Now the package is more friendly for web projects. - # [7.1.6] - Add enableUnfocusOnTapOutside field to RawEditor and Editor widgets. diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart index 4888da9f..cf04a462 100644 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ b/flutter_quill_extensions/lib/embeds/builders.dart @@ -7,7 +7,10 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/translations.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:math_keyboard/math_keyboard.dart'; +import 'package:universal_html/html.dart' as html; +import '../shims/dart_ui_fake.dart' + if (dart.library.html) '../shims/dart_ui_real.dart' as ui; import 'utils.dart'; import 'widgets/image.dart'; import 'widgets/image_resizer.dart'; @@ -145,6 +148,41 @@ class ImageEmbedBuilder extends EmbedBuilder { } } +class ImageEmbedBuilderWeb extends EmbedBuilder { + ImageEmbedBuilderWeb({this.constraints}) + : assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); + + final BoxConstraints? constraints; + + @override + String get key => BlockEmbed.imageType; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + ) { + final imageUrl = node.value.data; + + ui.platformViewRegistry.registerViewFactory(imageUrl, (viewId) { + return html.ImageElement() + ..src = imageUrl + ..style.height = 'auto' + ..style.width = 'auto'; + }); + + return ConstrainedBox( + constraints: constraints ?? BoxConstraints.loose(const Size(200, 200)), + child: HtmlElementView( + viewType: imageUrl, + ), + ); + } +} + class VideoEmbedBuilder extends EmbedBuilder { VideoEmbedBuilder({this.onVideoInit}); diff --git a/flutter_quill_extensions/lib/embeds/embed_types.dart b/flutter_quill_extensions/lib/embeds/embed_types.dart index 814b77b6..6a48f066 100644 --- a/flutter_quill_extensions/lib/embeds/embed_types.dart +++ b/flutter_quill_extensions/lib/embeds/embed_types.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -18,3 +19,28 @@ enum MediaPickSetting { Camera, Video, } + +typedef MediaFileUrl = String; +typedef MediaFilePicker = Future Function(QuillMediaType mediaType); +typedef MediaPickedCallback = Future Function(QuillFile file); + +enum QuillMediaType { image, video } + +extension QuillMediaTypeX on QuillMediaType { + bool get isImage => this == QuillMediaType.image; + bool get isVideo => this == QuillMediaType.video; +} + +/// Represents a file data which returned by file picker. +class QuillFile { + QuillFile({ + required this.name, + this.path = '', + Uint8List? bytes, + }) : assert(name.isNotEmpty), + bytes = bytes ?? Uint8List(0); + + final String name; + final String path; + final Uint8List bytes; +} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart new file mode 100644 index 00000000..837ca825 --- /dev/null +++ b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart @@ -0,0 +1,452 @@ +//import 'dart:io'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill/translations.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../embed_types.dart'; + +/// Widget which combines [ImageButton] and [VideButton] widgets. This widget +/// has more customization and uses dialog similar to one which is used +/// on [http://quilljs.com]. +class MediaButton extends StatelessWidget { + const MediaButton({ + required this.controller, + required this.icon, + this.type = QuillMediaType.image, + this.iconSize = kDefaultIconSize, + this.fillColor, + this.mediaFilePicker = _defaultMediaPicker, + this.onMediaPickedCallback, + this.iconTheme, + this.dialogTheme, + this.tooltip, + this.childrenSpacing = 16.0, + this.labelText, + this.hintText, + this.submitButtonText, + this.submitButtonSize, + this.galleryButtonText, + this.linkButtonText, + this.autovalidateMode = AutovalidateMode.disabled, + Key? key, + this.validationMessage, + }) : assert(type == QuillMediaType.image, + 'Video selection is not supported yet'), + super(key: key); + + final QuillController controller; + final IconData icon; + final double iconSize; + final Color? fillColor; + final QuillMediaType type; + final QuillIconTheme? iconTheme; + final QuillDialogTheme? dialogTheme; + final String? tooltip; + final MediaFilePicker mediaFilePicker; + final MediaPickedCallback? onMediaPickedCallback; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The text of label in link add mode. + final String? labelText; + + /// The hint text for link [TextField]. + final String? hintText; + + /// The text of the submit button. + final String? submitButtonText; + + /// The size of dialog buttons. + final Size? submitButtonSize; + + /// The text of the gallery button [MediaSourceSelectorDialog]. + final String? galleryButtonText; + + /// The text of the link button [MediaSourceSelectorDialog]. + final String? linkButtonText; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? fillColor ?? theme.canvasColor; + + return QuillIconButton( + icon: Icon(icon, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _onPressedHandler(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + if (onMediaPickedCallback != null) { + final mediaSource = await showDialog( + context: context, + builder: (_) => MediaSourceSelectorDialog( + dialogTheme: dialogTheme, + galleryButtonText: galleryButtonText, + linkButtonText: linkButtonText, + ), + ); + if (mediaSource != null) { + if (mediaSource == MediaPickSetting.Gallery) { + await _pickImage(); + } else { + _inputLink(context); + } + } + } else { + _inputLink(context); + } + } + + Future _pickImage() async { + if (!(kIsWeb || isMobile() || isDesktop())) { + throw UnsupportedError( + 'Unsupported target platform: ${defaultTargetPlatform.name}'); + } + + final mediaFileUrl = await _pickMediaFileUrl(); + + if (mediaFileUrl != null) { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + controller.replaceText( + index, length, BlockEmbed.image(mediaFileUrl), null); + } + } + + Future _pickMediaFileUrl() async { + final mediaFile = await mediaFilePicker(type); + return mediaFile != null ? onMediaPickedCallback?.call(mediaFile) : null; + } + + void _inputLink(BuildContext context) { + showDialog( + context: context, + builder: (_) => MediaLinkDialog( + dialogTheme: dialogTheme, + labelText: labelText, + hintText: hintText, + buttonText: submitButtonText, + buttonSize: submitButtonSize, + childrenSpacing: childrenSpacing, + autovalidateMode: autovalidateMode, + validationMessage: validationMessage, + ), + ).then(_linkSubmitted); + } + + void _linkSubmitted(String? value) { + if (value != null && value.isNotEmpty) { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + final data = + type.isImage ? BlockEmbed.image(value) : BlockEmbed.video(value); + controller.replaceText(index, length, data, null); + } + } +} + +/// Provides a dialog for input link to media resource. +class MediaLinkDialog extends StatefulWidget { + const MediaLinkDialog({ + Key? key, + this.link, + this.dialogTheme, + this.childrenSpacing = 16.0, + this.labelText, + this.hintText, + this.buttonText, + this.buttonSize, + this.autovalidateMode = AutovalidateMode.disabled, + this.validationMessage, + }) : assert(childrenSpacing > 0), + super(key: key); + + final String? link; + final QuillDialogTheme? dialogTheme; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The text of label in link add mode. + final String? labelText; + + /// The hint text for link [TextField]. + final String? hintText; + + /// The text of the submit button. + final String? buttonText; + + /// The size of dialog buttons. + final Size? buttonSize; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + @override + State createState() => _MediaLinkDialogState(); +} + +class _MediaLinkDialogState extends State { + final _linkFocus = FocusNode(); + final _linkController = TextEditingController(); + + @override + void dispose() { + _linkFocus.dispose(); + _linkController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final constraints = widget.dialogTheme?.linkDialogConstraints ?? + () { + final mediaQuery = MediaQuery.of(context); + final maxWidth = + kIsWeb ? mediaQuery.size.width / 4 : mediaQuery.size.width - 80; + return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); + }(); + + final buttonStyle = widget.buttonSize != null + ? Theme.of(context) + .elevatedButtonTheme + .style + ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) + : widget.dialogTheme?.buttonStyle; + + final isWrappable = widget.dialogTheme?.isWrappable ?? false; + + final children = [ + Text(widget.labelText ?? 'Enter media'.i18n), + UtilityWidgets.maybeWidget( + enabled: !isWrappable, + wrapper: (child) => Expanded( + child: child, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), + child: TextFormField( + controller: _linkController, + focusNode: _linkFocus, + style: widget.dialogTheme?.inputTextStyle, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + labelStyle: widget.dialogTheme?.labelTextStyle, + hintText: widget.hintText, + ), + autofocus: true, + autovalidateMode: widget.autovalidateMode, + validator: _validateLink, + onChanged: _linkChanged, + ), + ), + ), + ElevatedButton( + onPressed: _canPress() ? _submitLink : null, + style: buttonStyle, + child: Text(widget.buttonText ?? 'Ok'.i18n), + ), + ]; + + return Dialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, + shape: widget.dialogTheme?.shape ?? + DialogTheme.of(context).shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + child: ConstrainedBox( + constraints: constraints, + child: Padding( + padding: + widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), + child: isWrappable + ? Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, + children: children, + ) + : Row( + children: children, + ), + ), + ), + ); + } + + bool _canPress() => _validateLink(_linkController.text) == null; + + void _linkChanged(String value) { + setState(() { + _linkController.text = value; + }); + } + + void _submitLink() => Navigator.pop(context, _linkController.text); + + String? _validateLink(String? value) { + if ((value?.isEmpty ?? false) || + !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { + return widget.validationMessage ?? 'That is not a valid URL'; + } + + return null; + } +} + +/// Media souce selector. +class MediaSourceSelectorDialog extends StatelessWidget { + const MediaSourceSelectorDialog({ + Key? key, + this.dialogTheme, + this.galleryButtonText, + this.linkButtonText, + }) : super(key: key); + + final QuillDialogTheme? dialogTheme; + + /// The text of the gallery button [MediaSourceSelectorDialog]. + final String? galleryButtonText; + + /// The text of the link button [MediaSourceSelectorDialog]. + final String? linkButtonText; + + @override + Widget build(BuildContext context) { + final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? + () { + final mediaQuery = MediaQuery.of(context); + double maxWidth, maxHeight; + if (kIsWeb) { + maxWidth = mediaQuery.size.width / 7; + maxHeight = mediaQuery.size.height / 7; + } else { + maxWidth = mediaQuery.size.width - 80; + maxHeight = maxWidth / 2; + } + return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); + }(); + + final shape = dialogTheme?.shape ?? + DialogTheme.of(context).shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); + + return Dialog( + backgroundColor: dialogTheme?.dialogBackgroundColor, + shape: shape, + child: ConstrainedBox( + constraints: constraints, + child: Padding( + padding: dialogTheme?.mediaSelectorDialogPadding ?? + const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButtonWithIcon( + icon: Icons.collections, + label: galleryButtonText ?? 'Gallery'.i18n, + onPressed: () => + Navigator.pop(context, MediaPickSetting.Gallery), + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextButtonWithIcon( + icon: Icons.link, + label: linkButtonText ?? 'Link'.i18n, + onPressed: () => + Navigator.pop(context, MediaPickSetting.Link), + ), + ) + ], + ), + ), + ), + ); + } +} + +class TextButtonWithIcon extends StatelessWidget { + const TextButtonWithIcon({ + required this.label, + required this.icon, + required this.onPressed, + this.textStyle, + Key? key, + }) : super(key: key); + + final String label; + final IconData icon; + final VoidCallback onPressed; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; + final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; + final buttonStyle = TextButtonTheme.of(context).style; + final shape = buttonStyle?.shape?.resolve({}) ?? + const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4))); + return Material( + shape: shape, + textStyle: textStyle ?? + theme.textButtonTheme.style?.textStyle?.resolve({}) ?? + theme.textTheme.labelLarge, + elevation: buttonStyle?.elevation?.resolve({}) ?? 0, + child: InkWell( + customBorder: shape, + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon), + SizedBox(height: gap), + Flexible(child: Text(label)), + ], + ), + ), + ), + ); + } +} + +/// Default file picker. +Future _defaultMediaPicker(QuillMediaType mediaType) async { + final pickedFile = mediaType.isImage + ? await ImagePicker().pickImage(source: ImageSource.gallery) + : await ImagePicker().pickVideo(source: ImageSource.gallery); + + if (pickedFile != null) { + return QuillFile( + name: pickedFile.name, + path: pickedFile.path, + bytes: await pickedFile.readAsBytes(), + ); + } + + return null; +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/image.dart b/flutter_quill_extensions/lib/embeds/widgets/image.dart index d4df2a4c..658c5050 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/image.dart +++ b/flutter_quill_extensions/lib/embeds/widgets/image.dart @@ -21,7 +21,7 @@ String getImageStyleString(QuillController controller) { final String? s = controller .getAllSelectionStyles() .firstWhere((s) => s.attributes.containsKey(Attribute.style.key), - orElse: () => Style()) + orElse: Style.new) .attributes[Attribute.style.key] ?.value; return s ?? ''; diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 12b3bd82..fdbc54b2 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -15,18 +15,24 @@ export 'embeds/toolbar/camera_button.dart'; export 'embeds/toolbar/formula_button.dart'; export 'embeds/toolbar/image_button.dart'; export 'embeds/toolbar/image_video_utils.dart'; +export 'embeds/toolbar/media_button.dart'; export 'embeds/toolbar/video_button.dart'; export 'embeds/utils.dart'; class FlutterQuillEmbeds { - static List builders( - {void Function(GlobalKey videoContainerKey)? onVideoInit}) => + static List builders({ + void Function(GlobalKey videoContainerKey)? onVideoInit, + }) => [ ImageEmbedBuilder(), VideoEmbedBuilder(onVideoInit: onVideoInit), FormulaEmbedBuilder(), ]; + static List webBuilders() => [ + ImageEmbedBuilderWeb(), + ]; + static List buttons({ bool showImageButton = true, bool showVideoButton = true, @@ -43,58 +49,58 @@ class FlutterQuillEmbeds { FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, WebVideoPickImpl? webVideoPickImpl, - }) { - return [ - if (showImageButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton( - icon: Icons.image, - iconSize: toolbarIconSize, - tooltip: imageButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ), - if (showVideoButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton( - icon: Icons.movie_creation, - iconSize: toolbarIconSize, - tooltip: videoButtonTooltip, - controller: controller, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ), - if ((onImagePickCallback != null || onVideoPickCallback != null) && - showCameraButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( - icon: Icons.photo_camera, - iconSize: toolbarIconSize, - tooltip: cameraButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl, - cameraPickSettingSelector: cameraPickSettingSelector, - iconTheme: iconTheme, - ), - if (showFormulaButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => FormulaButton( - icon: Icons.functions, - iconSize: toolbarIconSize, - tooltip: formulaButtonTooltip, - controller: controller, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ) - ]; - } + }) => + [ + if (showImageButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton( + icon: Icons.image, + iconSize: toolbarIconSize, + tooltip: imageButtonTooltip, + controller: controller, + onImagePickCallback: onImagePickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ), + if (showVideoButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton( + icon: Icons.movie_creation, + iconSize: toolbarIconSize, + tooltip: videoButtonTooltip, + controller: controller, + onVideoPickCallback: onVideoPickCallback, + filePickImpl: filePickImpl, + webVideoPickImpl: webImagePickImpl, + mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ), + if ((onImagePickCallback != null || onVideoPickCallback != null) && + showCameraButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( + icon: Icons.photo_camera, + iconSize: toolbarIconSize, + tooltip: cameraButtonTooltip, + controller: controller, + onImagePickCallback: onImagePickCallback, + onVideoPickCallback: onVideoPickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + webVideoPickImpl: webVideoPickImpl, + cameraPickSettingSelector: cameraPickSettingSelector, + iconTheme: iconTheme, + ), + if (showFormulaButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + FormulaButton( + icon: Icons.functions, + iconSize: toolbarIconSize, + tooltip: formulaButtonTooltip, + controller: controller, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ) + ]; } diff --git a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart new file mode 100644 index 00000000..baaf9ebd --- /dev/null +++ b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart @@ -0,0 +1,23 @@ +// ignore_for_file: avoid_classes_with_only_static_members, camel_case_types, lines_longer_than_80_chars + +import 'package:universal_html/html.dart' as html; + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as available. + +typedef PlatroformViewFactory = html.Element Function(int viewId); + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static dynamic registerViewFactory( + String viewTypeId, PlatroformViewFactory viewFactory) {} +} + +/// Shim for web_ui engine.AssetManager +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + static dynamic getAssetUrl(String asset) {} +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_real.dart b/flutter_quill_extensions/lib/shims/dart_ui_real.dart new file mode 100644 index 00000000..69c06ee2 --- /dev/null +++ b/flutter_quill_extensions/lib/shims/dart_ui_real.dart @@ -0,0 +1 @@ +export 'dart:ui'; diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 1d7d7606..48ac50ac 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.3.0 +version: 0.3.1 homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions @@ -12,15 +12,18 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.1.9 + flutter_quill: ^7.1.10 - image_picker: ^0.8.5+3 + file_picker: ^5.2.10 + image_picker: ^0.8.7+3 photo_view: ^0.14.0 - video_player: ^2.4.2 - youtube_player_flutter: ^8.1.1 + video_player: ^2.6.1 + youtube_player_flutter: ^8.1.2 gallery_saver: ^2.3.2 math_keyboard: ^0.1.8 string_validator: ^1.0.0 + universal_ui: ^0.0.8 + universal_html: ^2.2.1 url_launcher: ^6.1.9 dev_dependencies: diff --git a/lib/extensions.dart b/lib/extensions.dart index 12e961c2..b7c2b0af 100644 --- a/lib/extensions.dart +++ b/lib/extensions.dart @@ -4,3 +4,4 @@ export 'src/models/documents/nodes/leaf.dart' hide Text; export 'src/models/rules/insert.dart'; export 'src/utils/platform.dart'; export 'src/utils/string.dart'; +export 'src/utils/widgets.dart'; diff --git a/lib/src/models/themes/quill_dialog_theme.dart b/lib/src/models/themes/quill_dialog_theme.dart index 47d732b9..1552c5f5 100644 --- a/lib/src/models/themes/quill_dialog_theme.dart +++ b/lib/src/models/themes/quill_dialog_theme.dart @@ -10,7 +10,9 @@ class QuillDialogTheme with Diagnosticable { this.shape, this.buttonStyle, this.linkDialogConstraints, - this.imageDialogConstraints, + this.linkDialogPadding = const EdgeInsets.all(16), + this.mediaSelectorDialogConstraints, + this.mediaSelectorDialogPadding = const EdgeInsets.all(16), this.isWrappable = false, this.runSpacing = 8.0, }) : assert(runSpacing >= 0); @@ -34,8 +36,14 @@ class QuillDialogTheme with Diagnosticable { /// Constrains for [LinkStyleDialog]. final BoxConstraints? linkDialogConstraints; - /// Constrains for [EmbedImageDialog]. - final BoxConstraints? imageDialogConstraints; + /// The padding for content of [LinkStyleDialog]. + final EdgeInsetsGeometry linkDialogPadding; + + /// Constrains for [MediaSourceSelectorDialog]. + final BoxConstraints? mediaSelectorDialogConstraints; + + /// The padding for content of [MediaSourceSelectorDialog]. + final EdgeInsetsGeometry mediaSelectorDialogPadding; /// Customizes this button's appearance. final ButtonStyle? buttonStyle; @@ -57,7 +65,9 @@ class QuillDialogTheme with Diagnosticable { ShapeBorder? shape, ButtonStyle? buttonStyle, BoxConstraints? linkDialogConstraints, + EdgeInsetsGeometry? linkDialogPadding, BoxConstraints? imageDialogConstraints, + EdgeInsetsGeometry? mediaDialogPadding, bool? isWrappable, double? runSpacing, }) { @@ -70,8 +80,11 @@ class QuillDialogTheme with Diagnosticable { buttonStyle: buttonStyle ?? this.buttonStyle, linkDialogConstraints: linkDialogConstraints ?? this.linkDialogConstraints, - imageDialogConstraints: - imageDialogConstraints ?? this.imageDialogConstraints, + linkDialogPadding: linkDialogPadding ?? this.linkDialogPadding, + mediaSelectorDialogConstraints: + imageDialogConstraints ?? mediaSelectorDialogConstraints, + mediaSelectorDialogPadding: + mediaDialogPadding ?? mediaSelectorDialogPadding, isWrappable: isWrappable ?? this.isWrappable, runSpacing: runSpacing ?? this.runSpacing, ); @@ -89,7 +102,10 @@ class QuillDialogTheme with Diagnosticable { other.shape == shape && other.buttonStyle == buttonStyle && other.linkDialogConstraints == linkDialogConstraints && - other.imageDialogConstraints == imageDialogConstraints && + other.linkDialogPadding == linkDialogPadding && + other.mediaSelectorDialogConstraints == + mediaSelectorDialogConstraints && + other.mediaSelectorDialogPadding == mediaSelectorDialogPadding && other.isWrappable == isWrappable && other.runSpacing == runSpacing; } @@ -102,7 +118,9 @@ class QuillDialogTheme with Diagnosticable { shape, buttonStyle, linkDialogConstraints, - imageDialogConstraints, + linkDialogPadding, + mediaSelectorDialogConstraints, + mediaSelectorDialogPadding, isWrappable, runSpacing, ); diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 507d6ebd..305e0152 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -63,6 +63,7 @@ extension Localization on String { 'Insert URL': 'Insert URL', 'Visit link': 'Visit link', 'Enter link': 'Enter link', + 'Enter media': 'Enter media', 'Edit': 'Edit', 'Apply': 'Apply', }, @@ -126,6 +127,7 @@ extension Localization on String { 'Insert URL': 'Insert URL', 'Visit link': 'Visit link', 'Enter link': 'Enter link', + 'Enter media': 'Enter media', 'Edit': 'Edit', 'Apply': 'Apply', }, diff --git a/lib/src/widgets/toolbar/link_style_button2.dart b/lib/src/widgets/toolbar/link_style_button2.dart index ad73152a..42aaff11 100644 --- a/lib/src/widgets/toolbar/link_style_button2.dart +++ b/lib/src/widgets/toolbar/link_style_button2.dart @@ -7,7 +7,6 @@ import '../../../translations.dart'; import '../../models/documents/attribute.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; -import '../../utils/widgets.dart'; import '../controller.dart'; import '../link.dart'; import '../toolbar.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 569616c4..24e9ed14 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.9 +version: 7.1.10 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill