From 748ddfba58a148692cef94c26ccdcd1c374a92cb Mon Sep 17 00:00:00 2001 From: bambinoua <130981115+MacDeveloper1@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:02:56 +0200 Subject: [PATCH] Add `ImageButton2` --- .../lib/embeds/toolbar/image_button2.dart | 299 ++++++++++++++++++ .../lib/flutter_quill_extensions.dart | 114 +++---- lib/flutter_quill.dart | 1 + 3 files changed, 358 insertions(+), 56 deletions(-) create mode 100644 flutter_quill_extensions/lib/embeds/toolbar/image_button2.dart diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_button2.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_button2.dart new file mode 100644 index 00000000..e6348966 --- /dev/null +++ b/flutter_quill_extensions/lib/embeds/toolbar/image_button2.dart @@ -0,0 +1,299 @@ +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'; +import 'image_video_utils.dart' hide LinkDialog; + +/// Alternative version of [ImageButton]. This widget has more customization +/// and uses dialog similar to one which is used on [http://quilljs.com]. +class ImageButton2 extends StatelessWidget { + const ImageButton2({ + required this.controller, + required this.icon, + this.iconSize = kDefaultIconSize, + this.fillColor, + this.onImagePickCallback, + this.webImagePickImpl, + this.filePickImpl, + this.mediaPickSettingSelector, + this.iconTheme, + this.dialogTheme, + this.tooltip, + this.childrenSpacing = 16.0, + this.contentPadding = const EdgeInsets.all(16), + this.constraints, + this.buttonSize, + this.labelText, + this.hintText, + this.buttonText, + this.autovalidateMode = AutovalidateMode.disabled, + Key? key, + this.validationMessage, + }) : super(key: key); + + final QuillController controller; + final IconData icon; + final double iconSize; + final Color? fillColor; + final OnImagePickCallback? onImagePickCallback; + final WebImagePickImpl? webImagePickImpl; + final FilePickImpl? filePickImpl; + final MediaPickSettingSelector? mediaPickSettingSelector; + final QuillIconTheme? iconTheme; + final QuillDialogTheme? dialogTheme; + final String? tooltip; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The padding for content of dialog. + final EdgeInsetsGeometry contentPadding; + + /// The constrains for dialog. + final BoxConstraints? constraints; + + /// The size of dialog buttons. + final Size? buttonSize; + + /// 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; + + 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 (onImagePickCallback != null) { + final selector = + mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; + final source = await selector(context); + if (source != null) { + if (source == MediaPickSetting.Gallery) { + _pickImage(context); + } else { + _typeLink(context); + } + } + } else { + _typeLink(context); + } + } + + void _pickImage(BuildContext context) { + ImageVideoUtils.handleImageButtonTap( + context, + controller, + ImageSource.gallery, + onImagePickCallback!, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + ); + } + + void _typeLink(BuildContext context) { + showDialog( + context: context, + builder: (_) => EmbedLinkDialog( + dialogTheme: dialogTheme, + constraints: constraints, + labelText: labelText, + childrenSpacing: childrenSpacing, + autovalidateMode: autovalidateMode, + validationMessage: validationMessage, + buttonSize: buttonSize, + ), + ).then(_linkSubmitted); + } + + void _linkSubmitted(String? value) { + if (value != null && value.isNotEmpty) { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + controller.replaceText(index, length, BlockEmbed.image(value), null); + } + } +} + +/// +class EmbedLinkDialog extends StatefulWidget { + const EmbedLinkDialog({ + Key? key, + this.link, + this.dialogTheme, + this.contentPadding = const EdgeInsets.all(16), + this.childrenSpacing = 16.0, + this.constraints, + this.buttonSize, + this.labelText, + this.hintText, + this.buttonText, + 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 padding for content of dialog. + final EdgeInsetsGeometry contentPadding; + + /// The constrains for dialog. + final BoxConstraints? constraints; + + /// The size of dialog buttons. + final Size? buttonSize; + + /// 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; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + @override + State createState() => _EmbedLinkDialogState(); +} + +class _EmbedLinkDialogState 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 link'.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.contentPadding, + 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; + } +} diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 12b3bd82..bef1ce7d 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -14,13 +14,15 @@ export 'embeds/embed_types.dart'; export 'embeds/toolbar/camera_button.dart'; export 'embeds/toolbar/formula_button.dart'; export 'embeds/toolbar/image_button.dart'; +export 'embeds/toolbar/image_button2.dart'; export 'embeds/toolbar/image_video_utils.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), @@ -43,58 +45,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/lib/flutter_quill.dart b/lib/flutter_quill.dart index bfc666f1..7cc3ef76 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -18,6 +18,7 @@ export 'src/models/themes/quill_custom_button.dart'; export 'src/models/themes/quill_dialog_theme.dart'; export 'src/models/themes/quill_icon_theme.dart'; export 'src/utils/embeds.dart'; +export 'src/utils/widgets.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart';