diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index fffc4abb..3411d5e7 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -13,6 +13,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:flutter_quill_extensions/presentation/models/config/toolbar/buttons/video.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -102,7 +103,7 @@ class _HomePageState extends State { context: context, builder: (context) => AlertDialog( content: Text(_controller.document.toPlainText([ - ...FlutterQuillEmbeds.builders(), + ...FlutterQuillEmbeds.editorBuilders(), TimeStampEmbedBuilderWidget() ])), ), @@ -254,7 +255,7 @@ class _HomePageState extends State { ), ), embedBuilders: [ - ...FlutterQuillEmbeds.builders(), + ...FlutterQuillEmbeds.editorBuilders(), TimeStampEmbedBuilderWidget() ], ), @@ -267,9 +268,11 @@ class _HomePageState extends State { if (kIsWeb) { return QuillToolbar( configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons( - onImagePickCallback: _onImagePickCallback, - webImagePickImpl: _webImagePickImpl, + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions( + onImagePickCallback: _onImagePickCallback, + webImagePickImpl: _webImagePickImpl, + ), ), buttonOptions: QuillToolbarButtonOptions( base: QuillToolbarBaseButtonOptions( @@ -283,9 +286,11 @@ class _HomePageState extends State { if (_isDesktop()) { return QuillToolbar( configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons( - onImagePickCallback: _onImagePickCallback, - filePickImpl: openFileSystemPickerForDesktop, + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions( + onImagePickCallback: _onImagePickCallback, + filePickImpl: openFileSystemPickerForDesktop, + ), ), showAlignmentButtons: true, buttonOptions: QuillToolbarButtonOptions( @@ -299,16 +304,20 @@ class _HomePageState extends State { } return QuillToolbar( configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons( - // provide a callback to enable picking images from device. - // if omit, "image" button only allows adding images from url. - // same goes for videos. - onImagePickCallback: _onImagePickCallback, - onVideoPickCallback: _onVideoPickCallback, - // uncomment to provide a custom "pick from" dialog. - // mediaPickSettingSelector: _selectMediaPickSetting, - // uncomment to provide a custom "pick from" dialog. - // cameraPickSettingSelector: _selectCameraPickSetting, + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions( + // provide a callback to enable picking images from device. + // if omit, "image" button only allows adding images from url. + // same goes for videos. + onImagePickCallback: _onImagePickCallback, + // uncomment to provide a custom "pick from" dialog. + // mediaPickSettingSelector: _selectMediaPickSetting, + // uncomment to provide a custom "pick from" dialog. + // cameraPickSettingSelector: _selectCameraPickSetting, + ), + videoButtonOptions: QuillToolbarVideoButtonOptions( + onVideoPickCallback: _onVideoPickCallback, + ), ), showAlignmentButtons: true, buttonOptions: QuillToolbarButtonOptions( @@ -437,12 +446,12 @@ class _HomePageState extends State { TextButton.icon( icon: const Icon(Icons.collections), label: const Text('Gallery'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), ), TextButton.icon( icon: const Icon(Icons.link), label: const Text('Link'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), ) ], ), @@ -461,12 +470,12 @@ class _HomePageState extends State { TextButton.icon( icon: const Icon(Icons.camera), label: const Text('Capture a photo'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Camera), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.camera), ), TextButton.icon( icon: const Icon(Icons.video_call), label: const Text('Capture a video'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Video), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.video), ) ], ), diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 5693f6b3..0f5af5d6 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -40,7 +40,7 @@ class _ReadOnlyPageState extends State { configurations: QuillEditorConfigurations( expands: false, padding: EdgeInsets.zero, - embedBuilders: FlutterQuillEmbeds.builders(), + embedBuilders: FlutterQuillEmbeds.editorBuilders(), scrollable: true, autoFocus: true, ), diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index fb96b01c..78c350fc 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -88,15 +88,17 @@ class _DemoScaffoldState extends State { if (_isDesktop()) { return QuillToolbar( configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons( - filePickImpl: openFileSystemPickerForDesktop, + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions( + filePickImpl: openFileSystemPickerForDesktop, + ), ), ), ); } return QuillToolbar( configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons(), + embedButtons: FlutterQuillEmbeds.toolbarButtons(), ), ); } diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 67c40380..67c8f430 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.6.0-dev.1 +- Breaking Changes, we have refactored most of the functions and it got renamed + ## 0.5.1 - Provide a way to use custom image provider for the image widgets diff --git a/flutter_quill_extensions/README.md b/flutter_quill_extensions/README.md index a685b8f0..e33f8362 100644 --- a/flutter_quill_extensions/README.md +++ b/flutter_quill_extensions/README.md @@ -1,22 +1,154 @@ # Flutter Quill Extensions -Helpers to support embed widgets in flutter_quill. See [Flutter Quill](https://pub.dev/packages/flutter_quill) for details of use. +A extensions for [flutter_quill](https://pub.dev/packages/flutter_quill) +to support embed widgets like image, formula, video and more. + +Currently the support for **Web** is limitied. + + Check [Flutter Quill](https://github.com/singerdmx/flutter-quill) for details of use. + + ## Table of Contents + +- [Flutter Quill Extensions](#flutter-quill-extensions) + - [Table of Contents](#table-of-contents) + - [About](#about) + - [Installation](#installation) + - [Platform Spesefic Configurations](#platform-spesefic-configurations) + - [Usage](#usage) + - [Features](#features) + - [Contributing](#contributing) + - [License](#license) + - [Acknowledgments](#acknowledgments) + + +## About +Flutter quill is a rich editor text and it's allow you to customize a lot of things, it has custom embed builders which allow you to render custom widgets in the editor
+this is a extensions to extends it functionallities by adding more features like images, videos, and more + +## Installation + +Before start using this package, please make sure to install +[flutter_quill](https://github.com/singerdmx/flutter-quill) package first and follow it's usage instructions. + +```yaml +dependencies: + flutter_quill_extensions: ^ +``` + +## Platform Spesefic Configurations + +> +> 1. We are using [`gal`](https://github.com/natsuk4ze/) plugin to save images. +> For this to work, you need to add the appropriate permissions +> to your `Info.plist` and `AndroidManifest.xml` files. +> See to add the needed lines. +> +> 2. We also use [`image_picker`](https://pub.dev/packages/image_picker) plugin for picking images so please make sure follow the instructions +> +> 3. For loading the image from the internet we need internet permission +> 1. For Android, you need to add some permissions in `AndroidManifest.xml`, Please follow this [link](https://developer.android.com/training/basics/network-ops/connecting) for more info, the internet permission included by default only for debugging so you need to follow this link to add it in the release version too. you should allow loading images and videos only for the `https` protocol but if you want http too then you need to configure your android application to accept `http` in the release mode, follow this [link](https://stackoverflow.com/questions/45940861/android-8-cleartext-http-traffic-not-permitted) for more info. +> 2. for macOS you also need to include a key in your `Info.plist`, please follow this [link](https://stackoverflow.com/a/61201081/18519412) to add the required configurations +> +> The extensions package also use [image_picker](https://pub.dev/packages/image_picker) which also require some configurations, follow this [link](https://pub.dev/packages/image_picker#installation). It's needed for Android, iOS, macOS, we must inform you that you can't pick photo using camera in desktop so make sure to handle that if you plan on add support for desktop, this might changed in the future and for more info follow this [link](https://pub.dev/packages/image_picker#windows-macos-and-linux)
+> ## Usage -Set the `embedBuilders` and `embedToolbar` params in `QuillEditor` and `QuillToolbar` with the +Before starting using this package you must follow the setup + +Set the `embedBuilders` and `embedToolbar` params in configurations of `QuillEditor` and `QuillToolbar` with the values provided by this repository. +**Quill toolbar**: +```dart +QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions( + onImagePickCallback: (file) async { + return file.path; + }, + ), + ), + ), +), ``` -QuillEditor.basic( - controller: controller, - embedBuilders: FlutterQuillEmbeds.builders(), -); + +**Quill Editor** +```dart +Expanded( + child: QuillEditor.basic( + configurations: QuillEditorConfigurations( + readOnly: true, + embedBuilders: FlutterQuillEmbeds.editorBuilders( + imageEmbedConfigurations: + const QuillEditorImageEmbedConfigurations( + forceUseMobileOptionMenuForImageClick: true, + ), + ), + ), + ), +) ``` +They both should be have a parent `QuillProvider` in the widget tree and setup properly
+Example: + +```dart +QuillProvider( + configurations: QuillConfigurations( + controller: _controller, + sharedConfigurations: const QuillSharedConfigurations(), + ), + child: Column( + children: [ + QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions(), + ), + ), + ), + Expanded( + child: QuillEditor.basic( + configurations: QuillEditorConfigurations( + padding: const EdgeInsets.all(16), + embedBuilders: FlutterQuillEmbeds.editorBuilders(), + ), + ), + ) + ], + ), +) ``` -QuillToolbar.basic( - controller: controller, - embedButtons: FlutterQuillEmbeds.buttons(), -); + +For web, use: +```dart +FlutterQuillEmbeds.editorsWebBuilders() ``` + +## Features + +```markdown +## Features + +- Easy to use and customizable +- Has the option to use custom image provider for the images +- Usefull utilities and widgets +- Handle different errors +``` + +## Contributing + +We welcome contributions! + +Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. + +## License + +This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Thanks to the [Flutter Team](https://flutter.dev/) +- Thanks to [flutter_quill](https://pub.dev/packages/flutter_quill) \ No newline at end of file diff --git a/flutter_quill_extensions/lib/core/exceptions.dart b/flutter_quill_extensions/lib/core/exceptions.dart new file mode 100644 index 00000000..3967adb7 --- /dev/null +++ b/flutter_quill_extensions/lib/core/exceptions.dart @@ -0,0 +1,12 @@ +// import 'package:meta/meta.dart'; + +// @immutable +// class NetworkException implements Exception { +// const NetworkException({required this.message}); + +// final String message; + +// @override +// String toString() => +// 'Error while loading something from the network: $message'; +// } diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart deleted file mode 100644 index 2492fd7c..00000000 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ /dev/null @@ -1,476 +0,0 @@ -import 'dart:io' show File; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_quill/extensions.dart' as base; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/translations.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 'embed_types.dart'; -import 'utils.dart'; -import 'widgets/image.dart'; -import 'widgets/image_resizer.dart'; -import 'widgets/video_app.dart'; -import 'widgets/youtube_video_app.dart'; - -class ImageEmbedBuilder extends EmbedBuilder { - ImageEmbedBuilder({ - required this.imageProviderBuilder, - required this.imageErrorWidgetBuilder, - required this.onImageRemovedCallback, - required this.shouldRemoveImageCallback, - this.forceUseMobileOptionMenu = false, - }); - final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback; - final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback; - final bool forceUseMobileOptionMenu; - final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; - final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; - - @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'); - - Widget image = const SizedBox.shrink(); - final imageUrl = standardizeImageUrl(node.value.data); - OptionalSize? imageSize; - final style = node.style.attributes['style']; - - if (style != null) { - final attrs = base.isMobile() - ? 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() - ? attrs[Attribute.mobileWidth] - : attrs[Attribute.width.key]) ?? - '', - ); - final height = double.tryParse( - (base.isMobile() - ? attrs[Attribute.mobileHeight] - : attrs[Attribute.height.key]) ?? - '', - ); - final alignment = base.getAlignment(base.isMobile() - ? attrs[Attribute.mobileAlignment] - : attrs[Attribute.alignment]); - final margin = (base.isMobile() - ? double.tryParse(Attribute.mobileMargin) - : double.tryParse(Attribute.margin)) ?? - 0.0; - - assert( - width != null && height != null, - 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: getQuillImageByUrl( - imageUrl, - width: width, - height: height, - alignment: alignment, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ), - ); - } - } - - if (imageSize == null) { - image = getQuillImageByUrl( - imageUrl, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - imageSize = OptionalSize((image as Image).width, image.height); - } - - if (!readOnly && (base.isMobile() || forceUseMobileOptionMenu)) { - 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(); - - final imageFile = File(imageUrl); - - // Call the remove check callback if set - if (await shouldRemoveImageCallback?.call(imageFile) == - 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 onImageRemovedCallback?.call(imageFile); - }, - ); - 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( - 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(), - ); - 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 web, desktop and other platforms - // and that is up to the developer - if (!base.isMobile() && forceUseMobileOptionMenu) { - return _menuOptionsForReadonlyImage( - context: context, - imageUrl: imageUrl, - image: image, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - } - return image; - } - - // We provide option menu for mobile platform excluding base64 image - return _menuOptionsForReadonlyImage( - context: context, - imageUrl: imageUrl, - image: image, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - } -} - -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, - TextStyle textStyle, - ) { - 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}); - - final void Function(GlobalKey videoContainerKey)? onVideoInit; - - @override - String get key => BlockEmbed.videoType; - - @override - Widget build( - BuildContext context, - QuillController controller, - base.Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); - - final videoUrl = node.value.data; - if (isYouTubeUrl(videoUrl)) { - return YoutubeVideoApp( - videoUrl: videoUrl, context: context, readOnly: readOnly); - } - return VideoApp( - videoUrl: videoUrl, - context: context, - readOnly: readOnly, - onVideoInit: onVideoInit, - ); - } -} - -class FormulaEmbedBuilder extends EmbedBuilder { - @override - String get key => BlockEmbed.formulaType; - - @override - Widget build( - BuildContext context, - QuillController controller, - base.Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); - - final mathController = MathFieldEditingController(); - return Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - // If the MathField is tapped, hides the built in keyboard - SystemChannels.textInput.invokeMethod('TextInput.hide'); - debugPrint(mathController.currentEditingValue()); - } - }, - child: MathField( - controller: mathController, - variables: const ['x', 'y', 'z'], - onChanged: (value) {}, - onSubmitted: (value) {}, - ), - ); - } -} - -Widget _menuOptionsForReadonlyImage({ - required BuildContext context, - required String imageUrl, - required Widget image, - required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, - required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, -}) { - return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - final saveOption = _SimpleDialogItem( - icon: Icons.save, - color: Colors.greenAccent, - text: 'Save'.i18n, - onPressed: () async { - imageUrl = appendFileExtensionToImageUrl(imageUrl); - final messenger = ScaffoldMessenger.of(context); - Navigator.of(context).pop(); - - final saveImageResult = await saveImage(imageUrl); - final imageSavedSuccessfully = saveImageResult.isSuccess; - - messenger.clearSnackBars(); - - if (!imageSavedSuccessfully) { - messenger.showSnackBar(SnackBar( - content: Text( - 'Error while saving image'.i18n, - ))); - return; - } - - var 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), - ), - ); - }, - ); - final zoomOption = _SimpleDialogItem( - icon: Icons.zoom_in, - color: Colors.cyanAccent, - text: 'Zoom'.i18n, - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ImageTapWrapper( - imageUrl: imageUrl, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ), - ), - ); - }, - ); - return Padding( - padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - child: SimpleDialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - children: [saveOption, zoomOption], - ), - ); - }, - ); - }, - child: image); -} - -class _SimpleDialogItem extends StatelessWidget { - const _SimpleDialogItem( - {required this.icon, - required this.color, - required this.text, - required this.onPressed, - Key? key}) - : super(key: key); - - final IconData icon; - final Color color; - final String text; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SimpleDialogOption( - onPressed: onPressed, - child: Row( - children: [ - Icon(icon, size: 36, color: color), - Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: - Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), - ), - ], - ), - ); - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart deleted file mode 100644 index 2c512567..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/translations.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class CameraButton extends StatelessWidget { - const CameraButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.onImagePickCallback, - this.onVideoPickCallback, - this.filePickImpl, - this.webImagePickImpl, - this.webVideoPickImpl, - this.cameraPickSettingSelector, - this.iconTheme, - this.tooltip, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnImagePickCallback? onImagePickCallback; - - final OnVideoPickCallback? onVideoPickCallback; - - final WebImagePickImpl? webImagePickImpl; - - final WebVideoPickImpl? webVideoPickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? cameraPickSettingSelector; - - final QuillIconTheme? iconTheme; - final String? tooltip; - - @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 QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _handleCameraButtonTap( - context, - controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ), - ); - } - - Future _handleCameraButtonTap( - BuildContext context, - QuillController controller, { - OnImagePickCallback? onImagePickCallback, - OnVideoPickCallback? onVideoPickCallback, - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - }) async { - if (onVideoPickCallback == null && onImagePickCallback == null) { - throw ArgumentError( - 'onImagePickCallback and onVideoPickCallback are both null', - ); - } - final selector = cameraPickSettingSelector ?? - (context) => showDialog( - context: context, - builder: (ctx) => AlertDialog( - contentPadding: EdgeInsets.zero, - backgroundColor: Colors.transparent, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (onImagePickCallback != null) - TextButton.icon( - icon: const Icon( - Icons.camera, - color: Colors.orangeAccent, - ), - label: Text('Camera'.i18n), - onPressed: () => - Navigator.pop(ctx, MediaPickSetting.Camera), - ), - if (onVideoPickCallback != null) - TextButton.icon( - icon: const Icon( - Icons.video_call, - color: Colors.cyanAccent, - ), - label: Text('Video'.i18n), - onPressed: () => - Navigator.pop(ctx, MediaPickSetting.Video), - ) - ], - ), - ), - ); - - final source = await selector(context); - if (source == null) { - return; - } - switch (source) { - case MediaPickSetting.Camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallback!, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - break; - case MediaPickSetting.Video: - await ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.camera, - onVideoPickCallback!, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, - ); - break; - case MediaPickSetting.Gallery: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'gallery is not related to camera button', - ); - case MediaPickSetting.Link: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'link is not related to camera button', - ); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart deleted file mode 100644 index 882d067c..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; - -class FormulaButton extends StatelessWidget { - const FormulaButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.iconTheme, - this.dialogTheme, - this.tooltip, - Key? key, - }) : super(key: key); - - final IconData icon; - - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - final String? tooltip; - - @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 QuillToolbarIconButton( - 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 { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - controller.replaceText(index, length, BlockEmbed.formula(''), null); - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart deleted file mode 100644 index af5f924f..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class ImageButton extends StatelessWidget { - const ImageButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.onImagePickCallback, - this.fillColor, - this.filePickImpl, - this.webImagePickImpl, - this.mediaPickSettingSelector, - this.iconTheme, - this.dialogTheme, - this.tooltip, - this.linkRegExp, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnImagePickCallback? onImagePickCallback; - - final WebImagePickImpl? webImagePickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? mediaPickSettingSelector; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - final String? tooltip; - final RegExp? linkRegExp; - - @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 QuillToolbarIconButton( - 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 { - final onImagePickCallbackRef = onImagePickCallback; - if (onImagePickCallbackRef == null) { - await _typeLink(context); - return; - } - final selector = - mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; - final source = await selector(context); - if (source == null) { - return; - } - switch (source) { - case MediaPickSetting.Gallery: - _pickImage(context); - break; - case MediaPickSetting.Link: - await _typeLink(context); - break; - case MediaPickSetting.Camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallbackRef, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - break; - case MediaPickSetting.Video: - throw ArgumentError( - 'Sorry but this is the Image button and not the video one', - ); - } - - // This will not work for the pick image using camera (bug fix) - // if (source != null) { - // if (source == MediaPickSetting.Gallery) { - - // } else { - // _typeLink(context); - // } - } - - void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.gallery, - onImagePickCallback!, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - - Future _typeLink(BuildContext context) async { - final value = await showDialog( - context: context, - builder: (_) => LinkDialog( - dialogTheme: dialogTheme, - linkRegExp: linkRegExp, - ), - ); - _linkSubmitted(value); - } - - 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); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart deleted file mode 100644 index f09d193b..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class VideoButton extends StatelessWidget { - const VideoButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.onVideoPickCallback, - this.fillColor, - this.filePickImpl, - this.webVideoPickImpl, - this.mediaPickSettingSelector, - this.iconTheme, - this.dialogTheme, - this.tooltip, - this.linkRegExp, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnVideoPickCallback? onVideoPickCallback; - - final WebVideoPickImpl? webVideoPickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? mediaPickSettingSelector; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - - final String? tooltip; - - final RegExp? linkRegExp; - - @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 QuillToolbarIconButton( - 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 (onVideoPickCallback != null) { - final selector = - mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; - final source = await selector(context); - if (source != null) { - if (source == MediaPickSetting.Gallery) { - _pickVideo(context); - } else { - await _typeLink(context); - } - } - } else { - await _typeLink(context); - } - } - - void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.gallery, - onVideoPickCallback!, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, - ); - - Future _typeLink(BuildContext context) async { - final value = await showDialog( - context: context, - builder: (_) => LinkDialog(dialogTheme: dialogTheme), - ); - _linkSubmitted(value); - } - - 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.video(value), null); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/utils.dart b/flutter_quill_extensions/lib/embeds/utils.dart deleted file mode 100644 index 9d92f149..00000000 --- a/flutter_quill_extensions/lib/embeds/utils.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:io' show File; - -import 'package:flutter/foundation.dart' show Uint8List, immutable; -import 'package:gal/gal.dart'; -import 'package:http/http.dart' as http; - -// I would like to orgnize the project structure and the code more -// but here I don't want to change too much since that is a community project - -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 isYouTubeUrl(String videoUrl) { - try { - final uri = Uri.parse(videoUrl); - return uri.host == 'www.youtube.com' || - uri.host == 'youtube.com' || - uri.host == 'youtu.be'; - } catch (_) { - return false; - } -} - -bool isImageBase64(String imageUrl) { - return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl); -} - -enum SaveImageResultMethod { network, localStorage } - -@immutable -class _SaveImageResult { - const _SaveImageResult({required this.isSuccess, required this.method}); - - final bool isSuccess; - final SaveImageResultMethod method; -} - -Future<_SaveImageResult> saveImage(String imageUrl) async { - final imageFile = File(imageUrl); - final hasPermission = await Gal.hasAccess(); - final imageExistsLocally = await imageFile.exists(); - if (!hasPermission) { - await Gal.requestAccess(); - } - if (!imageExistsLocally) { - final success = await _saveNetworkImageToLocal(imageUrl); - return _SaveImageResult( - isSuccess: success, - method: SaveImageResultMethod.network, - ); - } - final success = await _saveImageLocally(imageFile); - return _SaveImageResult( - isSuccess: success, - method: SaveImageResultMethod.localStorage, - ); -} - -Future _saveNetworkImageToLocal(String imageUrl) async { - try { - final response = await http.get( - Uri.parse(imageUrl), - ); - if (response.statusCode != 200) { - return false; - } - final imageBytes = response.bodyBytes; - await Gal.putImageBytes(imageBytes); - return true; - } catch (e) { - return false; - } -} - -Future _convertFileToUint8List(File file) async { - try { - final uint8list = await file.readAsBytes(); - return uint8list; - } catch (e) { - return Uint8List(0); - } -} - -Future _saveImageLocally(File imageFile) async { - try { - final imageBytes = await _convertFileToUint8List(imageFile); - await Gal.putImageBytes(imageBytes); - return true; - } catch (e) { - return false; - } -} diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 0fc6a54e..efc3a6ab 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -1,27 +1,45 @@ library flutter_quill_extensions; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; -import 'embeds/builders.dart'; -import 'embeds/embed_types.dart'; -import 'embeds/toolbar/camera_button.dart'; -import 'embeds/toolbar/formula_button.dart'; -import 'embeds/toolbar/image_button.dart'; -import 'embeds/toolbar/media_button.dart'; -import 'embeds/toolbar/video_button.dart'; +import 'presentation/embeds/editor/image/image.dart'; +import 'presentation/embeds/editor/image/image_web.dart'; +import 'presentation/embeds/editor/video.dart'; +import 'presentation/embeds/editor/webview.dart'; +import 'presentation/embeds/toolbar/camera_button.dart'; +import 'presentation/embeds/toolbar/formula_button.dart'; +import 'presentation/embeds/toolbar/image_button.dart'; +import 'presentation/embeds/toolbar/media_button.dart'; +import 'presentation/embeds/toolbar/video_button.dart'; +import 'presentation/models/config/editor/image.dart'; +import 'presentation/models/config/editor/video.dart'; +import 'presentation/models/config/editor/webview.dart'; +import 'presentation/models/config/toolbar/buttons/camera.dart'; +import 'presentation/models/config/toolbar/buttons/formula.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'; -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_video_utils.dart'; -export 'embeds/toolbar/media_button.dart'; -export 'embeds/toolbar/video_button.dart'; -export 'embeds/utils.dart'; +export '/presentation/models/config/editor/webview.dart'; +export './logic/extensions/controller.dart'; +export 'presentation/embeds/editor/unknown.dart'; +export 'presentation/embeds/embed_types.dart'; +export 'presentation/embeds/toolbar/camera_button.dart'; +export 'presentation/embeds/toolbar/formula_button.dart'; +export 'presentation/embeds/toolbar/image_button.dart'; +export 'presentation/embeds/toolbar/media_button.dart'; +export 'presentation/embeds/toolbar/utils/image_video_utils.dart'; +export 'presentation/embeds/toolbar/video_button.dart'; +export 'presentation/embeds/utils.dart'; +export 'presentation/models/config/editor/image.dart'; +export 'presentation/models/config/toolbar/buttons/image.dart'; +@immutable class FlutterQuillEmbeds { + const FlutterQuillEmbeds._(); + /// Returns a list of embed builders for QuillEditor. /// /// This method provides a collection of embed builders to enhance the @@ -32,179 +50,89 @@ class FlutterQuillEmbeds { /// /// **Note:** This method is not intended for web usage. /// For web-specific embeds, - /// use [webBuilders]. - /// - /// [onVideoInit] is a callback function that gets triggered when - /// a video is initialized. - /// You can use this to perform actions or setup configurations related - /// to video embedding. - /// - /// [onImageRemovedCallback] is called when an image is - /// removed from the editor. - /// By default, [onImageRemovedCallback] deletes the - /// temporary image file if - /// the platform is mobile and if it still exists. You - /// can customize this behavior - /// by passing your own function that handles the removal process. - /// - /// Example of [onImageRemovedCallback] customization: - /// ```dart - /// afterRemoveImageFromEditor: (imageFile) async { - /// // Your custom logic here - /// // or leave it empty to do nothing - /// } - /// ``` - /// - /// [shouldRemoveImageCallback] is a callback - /// function that is invoked when the - /// user attempts to remove an image from the editor. It allows you to control - /// whether the image should be removed based on your custom logic. - /// - /// Example of [shouldRemoveImageCallback] customization: - /// ```dart - /// shouldRemoveImageFromEditor: (imageFile) async { - /// // Show a confirmation dialog before removing the image - /// final isShouldRemove = await showYesCancelDialog( - /// context: context, - /// options: const YesOrCancelDialogOptions( - /// title: 'Deleting an image', - /// message: 'Are you sure you want' ' to delete this - /// image from the editor?', - /// ), - /// ); - /// - /// // Return `true` to allow image removal if the user confirms, otherwise - /// `false` - /// return isShouldRemove; - /// } - /// ``` - /// - /// [imageProviderBuilder] if you want to use custom image provider, please - /// pass a value to this property - /// By default we will use [NetworkImage] provider if the image url/path - /// is using http/https, if not then we will use [FileImage] provider - /// If you ovveride this make sure to handle the case where if the [imageUrl] - /// is in the local storage or it does exists in the system file - /// or use the same way we did it - /// - /// Example of [imageProviderBuilder] customization: - /// ```dart - /// imageProviderBuilder: (imageUrl) async { - /// // Example of using cached_network_image package - /// // Don't forgot to check if that image is local or network one - /// return CachedNetworkImageProvider(imageUrl); - /// } - /// ``` - /// - /// [imageErrorWidgetBuilder] if you want to show a custom widget based on the - /// exception that happen while loading the image, if it network image or - /// local one, and it will get called on all the images even in the photo - /// preview widget and not just in the quill editor - /// by default the default error from flutter framework will thrown + /// use [editorsWebBuilders]. /// - /// [forceUseMobileOptionMenuForImageClick] is a boolean - /// flag that, when set to `true`, - /// enforces the use of the mobile-specific option menu for image clicks in - /// other platforms like desktop, this option doesn't affect mobile. it will - /// not affect web - /// This option - /// can be used to override the default behavior based on the platform. /// /// The method returns a list of [EmbedBuilder] objects that can be used with /// QuillEditor /// to enable embedded content features like images, videos, and formulas. /// - /// Example usage: - /// ```dart - /// final embedBuilders = QuillEmbedBuilders.builders( - /// onVideoInit: (videoContainerKey) { - /// // Custom video initialization logic - /// }, - /// // Customize other callback functions as needed - /// ); /// /// final quillEditor = QuillEditor( /// // Other editor configurations /// embedBuilders: embedBuilders, /// ); /// ``` - static List builders({ - void Function(GlobalKey videoContainerKey)? onVideoInit, - ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, - ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback, - ImageEmbedBuilderProviderBuilder? imageProviderBuilder, - ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, - bool forceUseMobileOptionMenuForImageClick = false, - }) => - [ - ImageEmbedBuilder( - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - imageProviderBuilder: imageProviderBuilder, - forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick, - onImageRemovedCallback: onImageRemovedCallback ?? - (imageFile) async { - final mobile = isMobile(); - // If the platform is not mobile, return void; - // Since the mobile OS gives us a copy of the image - - // Note: We should remove the image on Flutter web - // since the behavior is similar to how it is on mobile, - // but since this builder is not for web, we will ignore it - if (!mobile) { - return; - } - - // On mobile OS (Android, iOS), the system will not give us - // direct access to the image; instead, - // it will give us the image - // in the temp directory of the application. So, we want to - // remove it when we no longer need it. - - // but on desktop we don't want to touch user files - // especially on macOS, where we can't even delete it without - // permission - - final isFileExists = await imageFile.exists(); - if (isFileExists) { - await imageFile.delete(); - } - }, - shouldRemoveImageCallback: shouldRemoveImageCallback, + /// + /// if you don't want image embed in your quill editor then please pass null + /// to [imageEmbedConfigurations]. same apply to [videoEmbedConfigurations] + static List editorBuilders({ + QuillEditorImageEmbedConfigurations? imageEmbedConfigurations = + const QuillEditorImageEmbedConfigurations(), + QuillEditorVideoEmbedConfigurations? videoEmbedConfigurations = + const QuillEditorVideoEmbedConfigurations(), + QuillEditorWebViewEmbedConfigurations? webViewEmbedConfigurations = + const QuillEditorWebViewEmbedConfigurations(), + }) { + if (kIsWeb) { + throw UnsupportedError( + 'The editorBuilders() is not for web, please use editorBuilders() ' + 'instead', + ); + } + return [ + if (imageEmbedConfigurations != null) + QuillEditorImageEmbedBuilder( + configurations: QuillEditorImageEmbedConfigurations( + imageErrorWidgetBuilder: + imageEmbedConfigurations.imageErrorWidgetBuilder, + imageProviderBuilder: imageEmbedConfigurations.imageProviderBuilder, + forceUseMobileOptionMenuForImageClick: + imageEmbedConfigurations.forceUseMobileOptionMenuForImageClick, + onImageRemovedCallback: + imageEmbedConfigurations.onImageRemovedCallback ?? + QuillEditorImageEmbedConfigurations + .defaultOnImageRemovedCallback, + shouldRemoveImageCallback: + imageEmbedConfigurations.shouldRemoveImageCallback, + ), ), - VideoEmbedBuilder(onVideoInit: onVideoInit), - FormulaEmbedBuilder(), - ]; + if (videoEmbedConfigurations != null) + QuillEditorVideoEmbedBuilder( + configurations: videoEmbedConfigurations, + ), + const QuillEditorFormulaEmbedBuilder(), + if (webViewEmbedConfigurations != null) + QuillEditorWebViewEmbedBuilder( + configurations: webViewEmbedConfigurations, + ) + ]; + } /// Returns a list of embed builders specifically designed for web support. /// /// [ImageEmbedBuilderWeb] is the embed builder for handling /// images on the web. /// - static List webBuilders() => [ - ImageEmbedBuilderWeb(), - ]; + static List editorsWebBuilders({ + QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations = + const QuillEditorWebImageEmbedConfigurations(), + }) { + if (!kIsWeb) { + throw UnsupportedError( + 'The editorsWebBuilders() is only for web, please use editorBuilders() ' + 'instead', + ); + } + return [ + if (imageEmbedConfigurations != null) const ImageEmbedBuilderWeb(), + ]; + } /// Returns a list of embed button builders to customize the toolbar buttons. /// - /// [showImageButton] determines whether the image button should be displayed. - /// [showVideoButton] determines whether the video button should be displayed. - /// [showCameraButton] determines whether the camera button should - /// be displayed. - /// [showFormulaButton] determines whether the formula button - /// should be displayed. - /// - /// [imageButtonTooltip] specifies the tooltip text for the image button. - /// [videoButtonTooltip] specifies the tooltip text for the video button. - /// [cameraButtonTooltip] specifies the tooltip text for the camera button. - /// [formulaButtonTooltip] specifies the tooltip text for the formula button. - /// - /// [onImagePickCallback] is a callback function called when an - /// image is picked. - /// [onVideoPickCallback] is a callback function called when a - /// video is picked. - /// - /// [mediaPickSettingSelector] allows customizing media pick settings. - /// [cameraPickSettingSelector] allows customizing camera pick settings. + /// If you don't want to show one of the buttons for soem reason, + /// pass null to the options of it /// /// Example of customizing media pick settings for the image button: /// ```dart @@ -222,110 +150,49 @@ class FlutterQuillEmbeds { /// } /// ``` /// - /// [filePickImpl] is an implementation for picking files. - /// [webImagePickImpl] is an implementation for picking web images. - /// [webVideoPickImpl] is an implementation for picking web videos. - /// - /// [imageLinkRegExp] is a regular expression to identify image links. - /// [videoLinkRegExp] is a regular expression to identify video links. /// /// The returned list contains embed button builders for the Quill toolbar. - static List buttons({ - bool showImageButton = true, - bool showVideoButton = true, - bool showCameraButton = true, - bool showImageMediaButton = false, - bool showFormulaButton = false, - String? imageButtonTooltip, - String? videoButtonTooltip, - String? cameraButtonTooltip, - String? formulaButtonTooltip, - OnImagePickCallback? onImagePickCallback, - OnVideoPickCallback? onVideoPickCallback, - MediaPickSettingSelector? mediaPickSettingSelector, - MediaPickSettingSelector? cameraPickSettingSelector, - MediaPickedCallback? onImageMediaPickedCallback, - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - WebVideoPickImpl? webVideoPickImpl, - RegExp? imageLinkRegExp, - RegExp? videoLinkRegExp, + /// the [formulaButtonOptions] will be disabled by default on web + static List toolbarButtons({ + QuillToolbarImageButtonOptions? imageButtonOptions = + const QuillToolbarImageButtonOptions(), + QuillToolbarVideoButtonOptions? videoButtonOptions = + const QuillToolbarVideoButtonOptions(), + QuillToolbarFormulaButtonOptions? formulaButtonOptions = + const QuillToolbarFormulaButtonOptions(), + QuillToolbarCameraButtonOptions? cameraButtonOptions, + QuillToolbarMediaButtonOptions? mediaButtonOptions, }) => [ - 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, - linkRegExp: imageLinkRegExp, + if (imageButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarImageButton( + controller: imageButtonOptions.controller ?? controller, + options: imageButtonOptions, ), - 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, - linkRegExp: videoLinkRegExp, + if (videoButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarVideoButton( + controller: videoButtonOptions.controller ?? controller, + options: videoButtonOptions, ), - 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 (cameraButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarCameraButton( + controller: cameraButtonOptions.controller ?? controller, + options: cameraButtonOptions, ), - if (showImageMediaButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => MediaButton( - controller: controller, - dialogTheme: dialogTheme, - iconTheme: iconTheme, - iconSize: toolbarIconSize, - onMediaPickedCallback: onImageMediaPickedCallback, - onImagePickCallback: onImagePickCallback ?? - (throw ArgumentError.notNull( - 'onImagePickCallback is required when showCameraButton is' - ' true', - )), - onVideoPickCallback: onVideoPickCallback ?? - (throw ArgumentError.notNull( - 'onVideoPickCallback is required when showCameraButton is' - ' true', - )), - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl, - icon: Icons.perm_media, + if (mediaButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarMediaButton( + controller: mediaButtonOptions.controller ?? controller, + options: mediaButtonOptions, ), - if (showFormulaButton) + if (formulaButtonOptions != null && !kIsWeb) (controller, toolbarIconSize, iconTheme, dialogTheme) => - FormulaButton( - icon: Icons.functions, - iconSize: toolbarIconSize, - tooltip: formulaButtonTooltip, - controller: controller, - iconTheme: iconTheme, - dialogTheme: dialogTheme, + QuillToolbarFormulaButton( + controller: formulaButtonOptions.controller ?? controller, + options: formulaButtonOptions, ), ]; } diff --git a/flutter_quill_extensions/lib/logic/extensions/controller.dart b/flutter_quill_extensions/lib/logic/extensions/controller.dart new file mode 100644 index 00000000..3fe39ac2 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/extensions/controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../presentation/embeds/editor/webview.dart'; + +extension QuillControllerExt on QuillController { + int get index => selection.baseOffset; + int get length => selection.extentOffset - index; + void insertWebViewBlock({ + required String initialUrl, + }) { + final block = BlockEmbed.custom( + QuillEditorWebViewBlockEmbed( + initialUrl, + ), + ); + + this + ..skipRequestKeyboard = true + ..replaceText( + index, + length, + block, + null, + ); + } + + void insertImageBlock({ + required String imageUrl, + }) { + this + ..skipRequestKeyboard = true + ..replaceText( + index, + length, + BlockEmbed.image(imageUrl), + null, + ); + } +} diff --git a/flutter_quill_extensions/lib/logic/services/exceptions.dart b/flutter_quill_extensions/lib/logic/services/exceptions.dart new file mode 100644 index 00000000..70933e10 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/exceptions.dart @@ -0,0 +1,23 @@ +import 'package:meta/meta.dart' show immutable; + +enum ImageSaverExceptionType { + accessDenied, + notEnoughSpace, + notSupportedFormat, + unexpected, + unknown; +} + +@immutable +class ImageSaverException implements Exception { + const ImageSaverException({ + required this.message, + required this.type, + }); + + final String message; + final ImageSaverExceptionType type; + + @override + String toString() => 'Error while saving image, error type: ${type.name}'; +} diff --git a/flutter_quill_extensions/lib/logic/services/image_saver.dart b/flutter_quill_extensions/lib/logic/services/image_saver.dart new file mode 100644 index 00000000..2417ae8c --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver.dart @@ -0,0 +1,7 @@ +abstract class ImageSaverInterface { + const ImageSaverInterface(); + Future saveLocalImage(String imageUrl); + Future saveImageFromNetwork(Uri imageUrl); + Future hasAccess({required bool toAlbum}); + Future requestAccess({required bool toAlbum}); +} diff --git a/flutter_quill_extensions/lib/logic/services/packages/gal.dart b/flutter_quill_extensions/lib/logic/services/packages/gal.dart new file mode 100644 index 00000000..aec93ff4 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/packages/gal.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart' show NetworkImageLoadException; +import 'package:gal/gal.dart' show Gal, GalException, GalExceptionType; +import 'package:http/http.dart' as http; + +import '../exceptions.dart'; +import '../image_saver.dart'; + +class ImageSaverGalImpl extends ImageSaverInterface { + @override + Future saveImageFromNetwork(Uri imageUrl) async { + try { + final response = await http.get( + imageUrl, + ); + if (response.statusCode != 200) { + throw NetworkImageLoadException( + statusCode: response.statusCode, + uri: imageUrl, + ); + } + final imageBytes = response.bodyBytes; + await Gal.putImageBytes(imageBytes); + } on GalException catch (e) { + throw ImageSaverException( + message: e.toString(), + type: e.type.toImageSaverExceptionType(), + ); + } catch (e) { + throw ImageSaverException( + message: e.toString(), + type: ImageSaverExceptionType.unknown, + ); + } + } + + @override + Future saveLocalImage(String imageUrl) async { + try { + await Gal.putImage(imageUrl); + } on GalException catch (e) { + throw ImageSaverException( + message: e.toString(), + type: e.type.toImageSaverExceptionType(), + ); + } catch (e) { + throw ImageSaverException( + message: e.toString(), + type: ImageSaverExceptionType.unknown, + ); + } + } + + @override + Future hasAccess({required bool toAlbum}) { + return Gal.hasAccess(toAlbum: toAlbum); + } + + @override + Future requestAccess({required bool toAlbum}) { + return Gal.requestAccess(toAlbum: toAlbum); + } +} + +extension GalExceptionTypeExt on GalExceptionType { + ImageSaverExceptionType toImageSaverExceptionType() { + switch (this) { + case GalExceptionType.accessDenied: + return ImageSaverExceptionType.accessDenied; + case GalExceptionType.notEnoughSpace: + return ImageSaverExceptionType.notEnoughSpace; + case GalExceptionType.notSupportedFormat: + return ImageSaverExceptionType.notSupportedFormat; + case GalExceptionType.unexpected: + return ImageSaverExceptionType.unexpected; + } + } +} diff --git a/flutter_quill_extensions/lib/logic/services/s_image_saver.dart b/flutter_quill_extensions/lib/logic/services/s_image_saver.dart new file mode 100644 index 00000000..487e06ac --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/s_image_saver.dart @@ -0,0 +1,33 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'image_saver.dart'; +import 'packages/gal.dart' show ImageSaverGalImpl; + +class ImageSaverService extends ImageSaverInterface { + final ImageSaverInterface _provider; + const ImageSaverService({ + required ImageSaverInterface impl, + }) : _provider = impl; + + factory ImageSaverService.gal() => ImageSaverService( + impl: ImageSaverGalImpl(), + ); + + static final _instance = ImageSaverService.gal(); + factory ImageSaverService.getInstance() => _instance; + + @override + Future hasAccess({bool toAlbum = false}) => + _provider.hasAccess(toAlbum: toAlbum); + + @override + Future requestAccess({bool toAlbum = false}) => + _provider.requestAccess(toAlbum: toAlbum); + + @override + Future saveImageFromNetwork(Uri imageUrl) => + _provider.saveImageFromNetwork(imageUrl); + + @override + Future saveLocalImage(String imageUrl) => + _provider.saveLocalImage(imageUrl); +} diff --git a/flutter_quill_extensions/lib/utils/quill_utils.dart b/flutter_quill_extensions/lib/logic/utils/quill_utils.dart similarity index 99% rename from flutter_quill_extensions/lib/utils/quill_utils.dart rename to flutter_quill_extensions/lib/logic/utils/quill_utils.dart index 1ee9caff..154be39a 100644 --- a/flutter_quill_extensions/lib/utils/quill_utils.dart +++ b/flutter_quill_extensions/lib/logic/utils/quill_utils.dart @@ -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 '../flutter_quill_extensions.dart'; +import '../../presentation/embeds/utils.dart'; class QuillImageUtilities { const QuillImageUtilities._(); diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart new file mode 100644 index 00000000..3e6de8f2 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:math_keyboard/math_keyboard.dart'; + +class FormulaEmbedBuilder extends EmbedBuilder { + const FormulaEmbedBuilder(); + @override + String get key => BlockEmbed.formulaType; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); + + final mathController = MathFieldEditingController(); + return Focus( + onFocusChange: (hasFocus) { + if (hasFocus) { + // If the MathField is tapped, hides the built in keyboard + SystemChannels.textInput.invokeMethod('TextInput.hide'); + debugPrint(mathController.currentEditingValue()); + } + }, + child: MathField( + controller: mathController, + variables: const ['x', 'y', 'z'], + onChanged: (value) {}, + onSubmitted: (value) {}, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart new file mode 100644 index 00000000..9fb3fbc2 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart @@ -0,0 +1,336 @@ +import 'dart:io' show File; + +import 'package:flutter/cupertino.dart'; +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'; +import 'package:flutter_quill/translations.dart'; + +import '../../../models/config/editor/image.dart'; +import '../../embed_types.dart'; +import '../../utils.dart'; +import '../../widgets/image.dart'; +import '../../widgets/image_resizer.dart'; +import '../../widgets/simple_dialog_item.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'); + + Widget image = const SizedBox.shrink(); + final imageUrl = standardizeImageUrl(node.value.data); + OptionalSize? imageSize; + final style = node.style.attributes['style']; + + if (style != null) { + final attrs = base.isMobile() + ? 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() + ? attrs[Attribute.mobileWidth] + : attrs[Attribute.width.key]) ?? + '', + ); + final height = double.tryParse( + (base.isMobile() + ? attrs[Attribute.mobileHeight] + : attrs[Attribute.height.key]) ?? + '', + ); + final alignment = base.getAlignment(base.isMobile() + ? attrs[Attribute.mobileAlignment] + : attrs[Attribute.alignment]); + final margin = (base.isMobile() + ? double.tryParse(Attribute.mobileMargin) + : double.tryParse(Attribute.margin)) ?? + 0.0; + + assert( + width != null && height != null, + 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: getQuillImageByUrl( + imageUrl, + width: width, + height: height, + alignment: alignment, + imageProviderBuilder: configurations.imageProviderBuilder, + imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + ), + ); + } + } + + if (imageSize == null) { + image = getQuillImageByUrl( + imageUrl, + imageProviderBuilder: configurations.imageProviderBuilder, + imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + ); + imageSize = OptionalSize((image as Image).width, image.height); + } + + if (!readOnly && + (base.isMobile() || + 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(); + + final imageFile = File(imageUrl); + + // Call the remove check callback if set + if (await configurations.shouldRemoveImageCallback + ?.call(imageFile) == + 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(imageFile); + }, + ); + 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( + 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(), + ); + 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 web, desktop and other platforms + // and that is up to the developer + if (!base.isMobile() && + 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, + imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + ); + } +} + +Widget _menuOptionsForReadonlyImage({ + required BuildContext context, + required String imageUrl, + required Widget image, + required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, + required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, +}) { + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) { + final saveOption = SimpleDialogItem( + icon: Icons.save, + color: Colors.greenAccent, + text: 'Save'.i18n, + onPressed: () async { + imageUrl = appendFileExtensionToImageUrl(imageUrl); + final messenger = ScaffoldMessenger.of(context); + Navigator.of(context).pop(); + + final saveImageResult = await saveImage(imageUrl); + 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), + ), + ); + }, + ); + final zoomOption = SimpleDialogItem( + icon: Icons.zoom_in, + color: Colors.cyanAccent, + text: 'Zoom'.i18n, + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ImageTapWrapper( + imageUrl: imageUrl, + imageProviderBuilder: imageProviderBuilder, + imageErrorWidgetBuilder: imageErrorWidgetBuilder, + ), + ), + ); + }, + ); + return Padding( + padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), + child: SimpleDialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + children: [saveOption, zoomOption], + ), + ); + }, + ); + }, + child: image, + ); +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart new file mode 100644 index 00000000..970bf762 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.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; + +class ImageEmbedBuilderWeb extends EmbedBuilder { + const ImageEmbedBuilderWeb({ + this.constraints, + }); + + final BoxConstraints? constraints; + + @override + String get key => BlockEmbed.imageType; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); + 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, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/shims/dart_ui_fake.dart new file mode 100644 index 00000000..c42e2eb5 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/shims/dart_ui_fake.dart @@ -0,0 +1,43 @@ +// 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) {} +// } + +class PlatformViewRegistry { + /// Register [viewType] as being created by the given [viewFactory]. + /// + /// [viewFactory] can be any function that takes an integer and optional + /// `params` and returns an `HTMLElement` DOM object. + bool registerViewFactory( + String viewType, + Function viewFactory, { + bool isVisible = true, + }) { + return false; + } + + /// Returns the view previously created for [viewId]. + /// + /// Throws if no view has been created for [viewId]. + Object getViewById(int viewId) { + return ''; + } +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_real.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/shims/dart_ui_real.dart similarity index 100% rename from flutter_quill_extensions/lib/shims/dart_ui_real.dart rename to flutter_quill_extensions/lib/presentation/embeds/editor/image/shims/dart_ui_real.dart diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart new file mode 100644 index 00000000..bac7a6f5 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart @@ -0,0 +1,19 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +class QuillEditorUnknownEmbedBuilder extends EmbedBuilder { + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + return const Text('Unknown embed builder'); + } + + @override + String get key => 'unknown'; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/video.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/video.dart new file mode 100644 index 00000000..35e927e8 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/video.dart @@ -0,0 +1,81 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:math_keyboard/math_keyboard.dart'; + +import '../../models/config/editor/video.dart'; +import '../utils.dart'; +import '../widgets/video_app.dart'; +import '../widgets/youtube_video_app.dart'; + +class QuillEditorVideoEmbedBuilder extends EmbedBuilder { + const QuillEditorVideoEmbedBuilder({ + required this.configurations, + }); + + final QuillEditorVideoEmbedConfigurations configurations; + + @override + String get key => BlockEmbed.videoType; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); + + final videoUrl = node.value.data; + if (isYouTubeUrl(videoUrl)) { + return YoutubeVideoApp( + videoUrl: videoUrl, context: context, readOnly: readOnly); + } + return VideoApp( + videoUrl: videoUrl, + context: context, + readOnly: readOnly, + onVideoInit: configurations.onVideoInit, + ); + } +} + +class QuillEditorFormulaEmbedBuilder extends EmbedBuilder { + const QuillEditorFormulaEmbedBuilder(); + @override + String get key => BlockEmbed.formulaType; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); + + final mathController = MathFieldEditingController(); + return Focus( + onFocusChange: (hasFocus) { + if (hasFocus) { + // If the MathField is tapped, hides the built in keyboard + SystemChannels.textInput.invokeMethod('TextInput.hide'); + debugPrint(mathController.currentEditingValue()); + } + }, + child: MathField( + controller: mathController, + variables: const ['x', 'y', 'z'], + onChanged: (value) {}, + onSubmitted: (value) {}, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart new file mode 100644 index 00000000..4df6cc55 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart @@ -0,0 +1,58 @@ +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'; +} diff --git a/flutter_quill_extensions/lib/embeds/embed_types.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart similarity index 86% rename from flutter_quill_extensions/lib/embeds/embed_types.dart rename to flutter_quill_extensions/lib/presentation/embeds/embed_types.dart index 9fa4184a..16774b00 100644 --- a/flutter_quill_extensions/lib/embeds/embed_types.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart @@ -6,19 +6,25 @@ import 'package:flutter/material.dart' typedef OnImagePickCallback = Future Function(File file); typedef OnVideoPickCallback = Future Function(File file); + +/// [FilePickImpl] is an implementation for picking files. typedef FilePickImpl = Future Function(BuildContext context); + +/// [WebImagePickImpl] is an implementation for picking web images. typedef WebImagePickImpl = Future Function( - OnImagePickCallback onImagePickCallback); + OnImagePickCallback onImagePickCallback, +); typedef WebVideoPickImpl = Future Function( - OnVideoPickCallback onImagePickCallback); + OnVideoPickCallback onImagePickCallback, +); typedef MediaPickSettingSelector = Future Function( BuildContext context); enum MediaPickSetting { - Gallery, - Link, - Camera, - Video, + gallery, + link, + camera, + video, } typedef MediaFileUrl = String; diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart new file mode 100644 index 00000000..690f0f66 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart @@ -0,0 +1,202 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/translations.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../models/config/toolbar/buttons/camera.dart'; +import '../embed_types.dart'; +import 'utils/image_video_utils.dart'; + +class QuillToolbarCameraButton extends StatelessWidget { + const QuillToolbarCameraButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + final QuillToolbarCameraButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.photo_camera; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Camera'.i18n; + // ('Camera'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler( + context, + controller, + onImagePickCallback: options.onImagePickCallback, + onVideoPickCallback: options.onVideoPickCallback, + filePickImpl: options.filePickImpl, + webImagePickImpl: options.webImagePickImpl, + ); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final iconTheme = _iconTheme(context); + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + if (childBuilder != null) { + childBuilder( + QuillToolbarCameraButtonOptions( + onImagePickCallback: options.onImagePickCallback, + onVideoPickCallback: options.onVideoPickCallback, + afterButtonPressed: _afterButtonPressed(context), + cameraPickSettingSelector: options.cameraPickSettingSelector, + filePickImpl: options.filePickImpl, + iconData: options.iconData, + fillColor: options.fillColor, + iconSize: options.iconSize, + iconTheme: options.iconTheme, + tooltip: options.tooltip, + webImagePickImpl: options.webImagePickImpl, + webVideoPickImpl: options.webVideoPickImpl, + ), + QuillToolbarCameraButtonExtraOptions( + controller: controller, + context: context, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + final theme = Theme.of(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler( + BuildContext context, + QuillController controller, { + OnImagePickCallback? onImagePickCallback, + OnVideoPickCallback? onVideoPickCallback, + FilePickImpl? filePickImpl, + WebImagePickImpl? webImagePickImpl, + }) async { + if (onVideoPickCallback == null && onImagePickCallback == null) { + throw ArgumentError( + 'onImagePickCallback and onVideoPickCallback are both null', + ); + } + final selector = options.cameraPickSettingSelector ?? + (context) => showDialog( + context: context, + builder: (ctx) => AlertDialog( + contentPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (onImagePickCallback != null) + TextButton.icon( + icon: const Icon( + Icons.camera, + color: Colors.orangeAccent, + ), + label: Text('Camera'.i18n), + onPressed: () => + Navigator.pop(ctx, MediaPickSetting.camera), + ), + if (onVideoPickCallback != null) + TextButton.icon( + icon: const Icon( + Icons.video_call, + color: Colors.cyanAccent, + ), + label: Text('Video'.i18n), + onPressed: () => + Navigator.pop(ctx, MediaPickSetting.video), + ) + ], + ), + ), + ); + + final source = await selector(context); + if (source == null) { + return; + } + switch (source) { + case MediaPickSetting.camera: + await ImageVideoUtils.handleImageButtonTap( + context, + controller, + ImageSource.camera, + onImagePickCallback!, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + ); + break; + case MediaPickSetting.video: + await ImageVideoUtils.handleVideoButtonTap( + context, + controller, + ImageSource.camera, + onVideoPickCallback!, + filePickImpl: filePickImpl, + webVideoPickImpl: options.webVideoPickImpl, + ); + break; + case MediaPickSetting.gallery: + throw ArgumentError( + 'Invalid MediaSetting for the camera button.\n' + 'gallery is not related to camera button', + ); + case MediaPickSetting.link: + throw ArgumentError( + 'Invalid MediaSetting for the camera button.\n' + 'link is not related to camera button', + ); + } + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart new file mode 100644 index 00000000..4a0341d4 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../models/config/toolbar/buttons/formula.dart'; + +class QuillToolbarFormulaButton extends StatelessWidget { + const QuillToolbarFormulaButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + final QuillToolbarFormulaButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.functions; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Insert formula'; + // ('Insert formula'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + if (childBuilder != null) { + return childBuilder( + QuillToolbarFormulaButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + fillColor: iconFillColor, + iconData: iconData, + iconSize: iconSize, + iconTheme: iconTheme, + tooltip: tooltip, + ), + QuillToolbarFormulaButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + + controller.replaceText(index, length, BlockEmbed.formula(''), null); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart new file mode 100644 index 00000000..88b54ee2 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart @@ -0,0 +1,182 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../models/config/toolbar/buttons/image.dart'; +import '../embed_types.dart'; +import 'utils/image_video_utils.dart'; + +class QuillToolbarImageButton extends StatelessWidget { + const QuillToolbarImageButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + + final QuillToolbarImageButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.image; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Insert image'; + // ('Insert Image'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + if (childBuilder != null) { + return childBuilder( + QuillToolbarImageButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + iconData: iconData, + iconSize: iconSize, + dialogTheme: options.dialogTheme, + filePickImpl: options.filePickImpl, + webImagePickImpl: options.webImagePickImpl, + fillColor: options.fillColor, + iconTheme: options.iconTheme, + linkRegExp: options.linkRegExp, + mediaPickSettingSelector: options.mediaPickSettingSelector, + onImagePickCallback: options.onImagePickCallback, + tooltip: options.tooltip, + ), + QuillToolbarImageButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + return QuillToolbarIconButton( + icon: Icon( + iconData, + size: iconSize, + color: iconColor, + ), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + final onImagePickCallbackRef = options.onImagePickCallback; + if (onImagePickCallbackRef == null) { + await _typeLink(context); + return; + } + final selector = options.mediaPickSettingSelector ?? + ImageVideoUtils.selectMediaPickSetting; + final source = await selector(context); + if (source == null) { + return; + } + switch (source) { + case MediaPickSetting.gallery: + _pickImage(context); + break; + case MediaPickSetting.link: + await _typeLink(context); + break; + case MediaPickSetting.camera: + await ImageVideoUtils.handleImageButtonTap( + context, + controller, + ImageSource.camera, + onImagePickCallbackRef, + filePickImpl: options.filePickImpl, + webImagePickImpl: options.webImagePickImpl, + ); + break; + case MediaPickSetting.video: + throw ArgumentError( + 'Sorry but this is the Image button and not the video one', + ); + } + } + + void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( + context, + controller, + ImageSource.gallery, + options.onImagePickCallback ?? + (throw ArgumentError( + 'onImagePickCallback should not be null', + )), + filePickImpl: options.filePickImpl, + webImagePickImpl: options.webImagePickImpl, + ); + + Future _typeLink(BuildContext context) async { + final value = await showDialog( + context: context, + builder: (_) => LinkDialog( + dialogTheme: options.dialogTheme, + linkRegExp: options.linkRegExp, + ), + ); + _linkSubmitted(value); + } + + 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); + } + } +} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart similarity index 63% rename from flutter_quill_extensions/lib/embeds/toolbar/media_button.dart rename to flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart index a5eae3dd..212bd39e 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:math' as math; import 'dart:ui'; @@ -8,145 +10,177 @@ import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/translations.dart'; import 'package:image_picker/image_picker.dart'; +import '../../models/config/toolbar/buttons/media_button.dart'; import '../embed_types.dart'; -import 'image_video_utils.dart'; +import 'utils/image_video_utils.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({ +class QuillToolbarMediaButton extends StatelessWidget { + QuillToolbarMediaButton({ required this.controller, - required this.onImagePickCallback, - required this.onVideoPickCallback, - required this.filePickImpl, - required this.webImagePickImpl, - required this.webVideoPickImpl, - 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, - this.dialogBarrierColor = Colors.black54, - Key? key, - this.validationMessage, - }) : assert(type == QuillMediaType.image, - 'Video selection is not supported yet'), - super(key: key); + required this.options, + super.key, + }) : assert(options.type == QuillMediaType.image, + 'Video selection is not supported yet'); 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; - final Color dialogBarrierColor; + final QuillToolbarMediaButtonOptions options; - /// The margin between child widgets in the dialog. - final double childrenSpacing; + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } - /// The text of label in link add mode. - final String? labelText; + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } - /// The hint text for link [TextField]. - final String? hintText; + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } - /// The text of the submit button. - final String? submitButtonText; + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } - /// The size of dialog buttons. - final Size? submitButtonSize; + (IconData, String) get _defaultData { + switch (options.type) { + case QuillMediaType.image: + return (Icons.perm_media, 'Photo media button'); + case QuillMediaType.video: + throw UnsupportedError('The video is not supported yet.'); + } + } - /// The text of the gallery button [MediaSourceSelectorDialog]. - final String? galleryButtonText; + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + _defaultData.$1; + } - /// The text of the link button [MediaSourceSelectorDialog]. - final String? linkButtonText; + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + _defaultData.$2; + // ('Camera'.i18n); + } - final AutovalidateMode autovalidateMode; - final String? validationMessage; - final OnImagePickCallback onImagePickCallback; - final FilePickImpl? filePickImpl; - final WebImagePickImpl? webImagePickImpl; - final OnVideoPickCallback onVideoPickCallback; - final WebVideoPickImpl? webVideoPickImpl; + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } @override Widget build(BuildContext context) { + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + final iconTheme = _iconTheme(context); + + if (childBuilder != null) { + return childBuilder( + QuillToolbarMediaButtonOptions( + type: options.type, + onMediaPickedCallback: options.onMediaPickedCallback, + onImagePickCallback: options.onImagePickCallback, + onVideoPickCallback: options.onVideoPickCallback, + iconData: iconData, + afterButtonPressed: _afterButtonPressed(context), + autovalidateMode: options.autovalidateMode, + childrenSpacing: options.childrenSpacing, + dialogBarrierColor: options.dialogBarrierColor, + dialogTheme: options.dialogTheme, + filePickImpl: options.filePickImpl, + fillColor: options.fillColor, + galleryButtonText: options.galleryButtonText, + iconTheme: iconTheme, + iconSize: iconSize, + hintText: options.hintText, + labelText: options.labelText, + submitButtonSize: options.submitButtonSize, + linkButtonText: options.linkButtonText, + mediaFilePicker: options.mediaFilePicker, + submitButtonText: options.submitButtonText, + validationMessage: options.validationMessage, + webImagePickImpl: options.webImagePickImpl, + webVideoPickImpl: options.webVideoPickImpl, + tooltip: options.tooltip, + ), + QuillToolbarMediaButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + final theme = Theme.of(context); - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? fillColor ?? theme.canvasColor; + + final iconColor = + options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = options.iconTheme?.iconUnselectedFillColor ?? + options.fillColor ?? + theme.canvasColor; return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), + icon: Icon(iconData, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, size: iconSize * 1.77, fillColor: iconFillColor, borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), + onPressed: () => _sharedOnPressed(context), ); } Future _onPressedHandler(BuildContext context) async { - if (onMediaPickedCallback == null) { + if (options.onMediaPickedCallback == null) { _inputLink(context); return; } final mediaSource = await showDialog( context: context, builder: (_) => MediaSourceSelectorDialog( - dialogTheme: dialogTheme, - galleryButtonText: galleryButtonText, - linkButtonText: linkButtonText, + dialogTheme: options.dialogTheme, + galleryButtonText: options.galleryButtonText, + linkButtonText: options.linkButtonText, ), ); if (mediaSource == null) { return; } switch (mediaSource) { - case MediaPickSetting.Gallery: + case MediaPickSetting.gallery: await _pickImage(); break; - case MediaPickSetting.Link: + case MediaPickSetting.link: _inputLink(context); break; - case MediaPickSetting.Camera: + case MediaPickSetting.camera: await ImageVideoUtils.handleImageButtonTap( context, controller, ImageSource.camera, - onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, + options.onImagePickCallback, + filePickImpl: options.filePickImpl, + webImagePickImpl: options.webImagePickImpl, ); break; - case MediaPickSetting.Video: + case MediaPickSetting.video: await ImageVideoUtils.handleVideoButtonTap( context, controller, ImageSource.camera, - onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, + options.onVideoPickCallback, + filePickImpl: options.filePickImpl, + webVideoPickImpl: options.webVideoPickImpl, ); break; } @@ -174,22 +208,24 @@ class MediaButton extends StatelessWidget { } Future _pickMediaFileUrl() async { - final mediaFile = await mediaFilePicker(type); - return mediaFile != null ? onMediaPickedCallback?.call(mediaFile) : null; + final mediaFile = await options.mediaFilePicker?.call(options.type); + return mediaFile != null + ? options.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, + dialogTheme: options.dialogTheme, + labelText: options.labelText, + hintText: options.hintText, + buttonText: options.submitButtonText, + buttonSize: options.submitButtonSize, + childrenSpacing: options.childrenSpacing, + autovalidateMode: options.autovalidateMode, + validationMessage: options.validationMessage, ), ).then(_linkSubmitted); } @@ -198,8 +234,9 @@ class MediaButton extends StatelessWidget { 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); + final data = options.type.isImage + ? BlockEmbed.image(value) + : BlockEmbed.video(value); controller.replaceText(index, length, data, null); } } @@ -208,7 +245,7 @@ class MediaButton extends StatelessWidget { /// Provides a dialog for input link to media resource. class MediaLinkDialog extends StatefulWidget { const MediaLinkDialog({ - Key? key, + super.key, this.link, this.dialogTheme, this.childrenSpacing = 16.0, @@ -218,8 +255,7 @@ class MediaLinkDialog extends StatefulWidget { this.buttonSize, this.autovalidateMode = AutovalidateMode.disabled, this.validationMessage, - }) : assert(childrenSpacing > 0), - super(key: key); + }) : assert(childrenSpacing > 0); final String? link; final QuillDialogTheme? dialogTheme; @@ -347,7 +383,7 @@ class _MediaLinkDialogState extends State { String? _validateLink(String? value) { if ((value?.isEmpty ?? false) || - !AutoFormatMultipleLinksRule.oneLineRegExp.hasMatch(value!)) { + !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { return widget.validationMessage ?? 'That is not a valid URL'; } @@ -358,11 +394,11 @@ class _MediaLinkDialogState extends State { /// Media souce selector. class MediaSourceSelectorDialog extends StatelessWidget { const MediaSourceSelectorDialog({ - Key? key, + super.key, this.dialogTheme, this.galleryButtonText, this.linkButtonText, - }) : super(key: key); + }); final QuillDialogTheme? dialogTheme; @@ -408,7 +444,7 @@ class MediaSourceSelectorDialog extends StatelessWidget { icon: Icons.collections, label: galleryButtonText ?? 'Gallery'.i18n, onPressed: () => - Navigator.pop(context, MediaPickSetting.Gallery), + Navigator.pop(context, MediaPickSetting.gallery), ), ), const SizedBox(width: 10), @@ -417,7 +453,7 @@ class MediaSourceSelectorDialog extends StatelessWidget { icon: Icons.link, label: linkButtonText ?? 'Link'.i18n, onPressed: () => - Navigator.pop(context, MediaPickSetting.Link), + Navigator.pop(context, MediaPickSetting.link), ), ) ], @@ -434,8 +470,8 @@ class TextButtonWithIcon extends StatelessWidget { required this.icon, required this.onPressed, this.textStyle, - Key? key, - }) : super(key: key); + super.key, + }); final String label; final IconData icon; @@ -450,7 +486,10 @@ class TextButtonWithIcon extends StatelessWidget { final buttonStyle = TextButtonTheme.of(context).style; final shape = buttonStyle?.shape?.resolve({}) ?? const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))); + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ); return Material( shape: shape, textStyle: textStyle ?? @@ -477,18 +516,18 @@ class TextButtonWithIcon extends StatelessWidget { } /// 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; -} +// 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/toolbar/image_video_utils.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart similarity index 88% rename from flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart rename to flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart index 5da99c6d..4b03d7d7 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart @@ -7,15 +7,16 @@ import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/translations.dart'; import 'package:image_picker/image_picker.dart'; -import '../embed_types.dart'; +import '../../../../logic/extensions/controller.dart'; +import '../../embed_types.dart'; class LinkDialog extends StatefulWidget { const LinkDialog({ this.dialogTheme, this.link, this.linkRegExp, - Key? key, - }) : super(key: key); + super.key, + }); final QuillDialogTheme? dialogTheme; final String? link; @@ -122,7 +123,7 @@ class ImageVideoUtils { color: Colors.orangeAccent, ), label: Text('Gallery'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), ), TextButton.icon( icon: const Icon( @@ -130,7 +131,7 @@ class ImageVideoUtils { color: Colors.cyanAccent, ), label: Text('Link'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link), + onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), ) ], ), @@ -146,16 +147,17 @@ class ImageVideoUtils { FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, }) async { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - String? imageUrl; if (kIsWeb) { - assert( - webImagePickImpl != null, - 'Please provide webImagePickImpl for Web ' - '(check out example directory for how to do it)'); - imageUrl = await webImagePickImpl!(onImagePickCallback); + if (webImagePickImpl != null) { + imageUrl = await webImagePickImpl(onImagePickCallback); + return; + } + final file = await ImagePicker().pickImage(source: ImageSource.gallery); + imageUrl = file?.path; + if (imageUrl == null) { + return; + } } else if (isMobile()) { imageUrl = await _pickImage(imageSource, onImagePickCallback); } else { @@ -164,9 +166,13 @@ class ImageVideoUtils { await _pickImageDesktop(context, filePickImpl!, onImagePickCallback); } - if (imageUrl != null) { - controller.replaceText(index, length, BlockEmbed.image(imageUrl), null); + if (imageUrl == null) { + return; } + + controller.insertImageBlock( + imageUrl: imageUrl, + ); } static Future _pickImage( @@ -182,9 +188,10 @@ class ImageVideoUtils { } static Future _pickImageDesktop( - BuildContext context, - FilePickImpl filePickImpl, - OnImagePickCallback onImagePickCallback) async { + BuildContext context, + FilePickImpl filePickImpl, + OnImagePickCallback onImagePickCallback, + ) async { final filePath = await filePickImpl(context); if (filePath == null || filePath.isEmpty) return null; @@ -207,9 +214,10 @@ class ImageVideoUtils { String? videoUrl; if (kIsWeb) { assert( - webVideoPickImpl != null, - 'Please provide webVideoPickImpl for Web ' - '(check out example directory for how to do it)'); + webVideoPickImpl != null, + 'Please provide webVideoPickImpl for Web ' + 'in the options of this button', + ); videoUrl = await webVideoPickImpl!(onVideoPickCallback); } else if (isMobile()) { videoUrl = await _pickVideo(videoSource, onVideoPickCallback); diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart new file mode 100644 index 00000000..5019ac6b --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart @@ -0,0 +1,155 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../models/config/toolbar/buttons/video.dart'; +import '../embed_types.dart'; +import 'utils/image_video_utils.dart'; + +class QuillToolbarVideoButton extends StatelessWidget { + const QuillToolbarVideoButton({ + required this.options, + required this.controller, + super.key, + }); + + final QuillController controller; + + final QuillToolbarVideoButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.movie_creation; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Insert video'; + // ('Insert video'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + if (childBuilder != null) { + return childBuilder( + QuillToolbarVideoButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + iconData: iconData, + dialogTheme: options.dialogTheme, + filePickImpl: options.filePickImpl, + fillColor: iconFillColor, + iconSize: options.iconSize, + linkRegExp: options.linkRegExp, + tooltip: options.tooltip, + mediaPickSettingSelector: options.mediaPickSettingSelector, + iconTheme: options.iconTheme, + onVideoPickCallback: options.onVideoPickCallback, + webVideoPickImpl: options.webVideoPickImpl, + ), + QuillToolbarVideoButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + if (options.onVideoPickCallback != null) { + final selector = options.mediaPickSettingSelector ?? + ImageVideoUtils.selectMediaPickSetting; + final source = await selector(context); + if (source != null) { + if (source == MediaPickSetting.gallery) { + _pickVideo(context); + } else { + await _typeLink(context); + } + } + } else { + await _typeLink(context); + } + } + + void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap( + context, + controller, + ImageSource.gallery, + options.onVideoPickCallback!, + filePickImpl: options.filePickImpl, + webVideoPickImpl: options.webVideoPickImpl, + ); + + Future _typeLink(BuildContext context) async { + final value = await showDialog( + context: context, + builder: (_) => LinkDialog( + dialogTheme: options.dialogTheme, + ), + ); + _linkSubmitted(value); + } + + 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.video(value), null); + } + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/utils.dart b/flutter_quill_extensions/lib/presentation/embeds/utils.dart new file mode 100644 index 00000000..c7adfc42 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/utils.dart @@ -0,0 +1,87 @@ +import 'dart:io' show File; + +import 'package:flutter/foundation.dart' show immutable; +import '../../logic/services/s_image_saver.dart'; + +// I would like to orgnize the project structure and the code more +// but here I don't want to change too much since that is a community project + +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 isYouTubeUrl(String videoUrl) { + try { + final uri = Uri.parse(videoUrl); + return uri.host == 'www.youtube.com' || + uri.host == 'youtube.com' || + uri.host == 'youtu.be'; + } catch (_) { + return false; + } +} + +bool isImageBase64(String imageUrl) { + return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl); +} + +enum SaveImageResultMethod { network, localStorage } + +@immutable +class SaveImageResult { + const SaveImageResult({required this.isSuccess, required this.method}); + + final bool isSuccess; + final SaveImageResultMethod method; +} + +Future saveImage(String imageUrl) async { + final imageSaverService = ImageSaverService.getInstance(); + final imageFile = File(imageUrl); + final hasPermission = await imageSaverService.hasAccess(); + final imageExistsLocally = await imageFile.exists(); + if (!hasPermission) { + await imageSaverService.requestAccess(); + } + if (!imageExistsLocally) { + try { + await imageSaverService.saveImageFromNetwork( + Uri.parse(imageUrl), + ); + return const SaveImageResult( + isSuccess: true, + method: SaveImageResultMethod.network, + ); + } catch (e) { + return const SaveImageResult( + isSuccess: false, + method: SaveImageResultMethod.network, + ); + } + } + try { + await imageSaverService.saveLocalImage(imageUrl); + return const SaveImageResult( + isSuccess: true, + method: SaveImageResultMethod.localStorage, + ); + } catch (e) { + return const SaveImageResult( + isSuccess: false, + method: SaveImageResultMethod.localStorage, + ); + } +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/image.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart similarity index 99% rename from flutter_quill_extensions/lib/embeds/widgets/image.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart index fe698a43..8e3b617d 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/image.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart @@ -99,6 +99,7 @@ class ImageTapWrapper extends StatelessWidget { required this.imageUrl, required this.imageProviderBuilder, required this.imageErrorWidgetBuilder, + super.key, }); final String imageUrl; diff --git a/flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart similarity index 88% rename from flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart index 9f84ad17..92f2fb7c 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart @@ -5,14 +5,14 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_quill/translations.dart'; class ImageResizer extends StatefulWidget { - const ImageResizer( - {required this.imageWidth, - required this.imageHeight, - required this.maxWidth, - required this.maxHeight, - required this.onImageResize, - Key? key}) - : super(key: key); + const ImageResizer({ + required this.imageWidth, + required this.imageHeight, + required this.maxWidth, + required this.maxHeight, + required this.onImageResize, + super.key, + }); final double? imageWidth; final double? imageHeight; @@ -21,10 +21,10 @@ class ImageResizer extends StatefulWidget { final Function(double, double) onImageResize; @override - _ImageResizerState createState() => _ImageResizerState(); + ImageResizerState createState() => ImageResizerState(); } -class _ImageResizerState extends State { +class ImageResizerState extends State { late double _width; late double _height; @@ -85,6 +85,7 @@ class _ImageResizerState extends State { value: value, max: max, divisions: 1000, + // Might need to be changed label: label.i18n, onChanged: (val) { setState(() { diff --git a/flutter_quill_extensions/lib/presentation/embeds/widgets/simple_dialog_item.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/simple_dialog_item.dart new file mode 100644 index 00000000..4053fc05 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/simple_dialog_item.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class SimpleDialogItem extends StatelessWidget { + const SimpleDialogItem({ + required this.icon, + required this.color, + required this.text, + required this.onPressed, + super.key, + }); + + final IconData icon; + final Color color; + final String text; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SimpleDialogOption( + onPressed: onPressed, + child: Row( + children: [ + Icon(icon, size: 36, color: color), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: + Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart similarity index 96% rename from flutter_quill_extensions/lib/embeds/widgets/video_app.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart index c65b9a78..895d40cd 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart @@ -13,6 +13,7 @@ class VideoApp extends StatefulWidget { required this.videoUrl, required this.context, required this.readOnly, + super.key, this.onVideoInit, }); @@ -22,10 +23,10 @@ class VideoApp extends StatefulWidget { final void Function(GlobalKey videoContainerKey)? onVideoInit; @override - _VideoAppState createState() => _VideoAppState(); + VideoAppState createState() => VideoAppState(); } -class _VideoAppState extends State { +class VideoAppState extends State { late VideoPlayerController _controller; GlobalKey videoContainerKey = GlobalKey(); diff --git a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart similarity index 78% rename from flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart index 02e53fbe..e278280b 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart @@ -6,18 +6,21 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart'; class YoutubeVideoApp extends StatefulWidget { const YoutubeVideoApp( - {required this.videoUrl, required this.context, required this.readOnly}); + {required this.videoUrl, + required this.context, + required this.readOnly, + super.key}); final String videoUrl; final BuildContext context; final bool readOnly; @override - _YoutubeVideoAppState createState() => _YoutubeVideoAppState(); + YoutubeVideoAppState createState() => YoutubeVideoAppState(); } -class _YoutubeVideoAppState extends State { - var _youtubeController; +class YoutubeVideoAppState extends State { + YoutubePlayerController? _youtubeController; @override void initState() { @@ -36,7 +39,9 @@ class _YoutubeVideoAppState extends State { @override Widget build(BuildContext context) { final defaultStyles = DefaultStyles.getInstance(context); - if (_youtubeController == null) { + final youtubeController = _youtubeController; + + if (youtubeController == null) { if (widget.readOnly) { return RichText( text: TextSpan( @@ -51,11 +56,11 @@ class _YoutubeVideoAppState extends State { text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); } - return Container( + return SizedBox( height: 300, child: YoutubePlayerBuilder( player: YoutubePlayer( - controller: _youtubeController, + controller: youtubeController, showVideoProgressIndicator: true, ), builder: (context, player) { @@ -74,6 +79,6 @@ class _YoutubeVideoAppState extends State { @override void dispose() { super.dispose(); - _youtubeController.dispose(); + _youtubeController?.dispose(); } } diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart new file mode 100644 index 00000000..5a30ab15 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart @@ -0,0 +1,136 @@ +import 'package:flutter_quill/extensions.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../embeds/embed_types.dart'; + +/// [QuillEditorImageEmbedConfigurations] for desktop, mobile and +/// other platforms +/// excluding web, it's configurations that is needed for the editor +/// +@immutable +class QuillEditorImageEmbedConfigurations { + const QuillEditorImageEmbedConfigurations({ + this.forceUseMobileOptionMenuForImageClick = false, + this.onImageRemovedCallback, + this.shouldRemoveImageCallback, + this.imageProviderBuilder, + this.imageErrorWidgetBuilder, + }); + + /// [onImageRemovedCallback] is called when an image is + /// removed from the editor. + /// By default, [onImageRemovedCallback] deletes the + /// temporary image file if + /// the platform is mobile and if it still exists. You + /// can customize this behavior + /// by passing your own function that handles the removal process. + /// + /// Example of [onImageRemovedCallback] customization: + /// ```dart + /// afterRemoveImageFromEditor: (imageFile) async { + /// // Your custom logic here + /// // or leave it empty to do nothing + /// } + /// ``` + /// + final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback; + + /// [shouldRemoveImageCallback] is a callback + /// function that is invoked when the + /// user attempts to remove an image from the editor. It allows you to control + /// whether the image should be removed based on your custom logic. + /// + /// Example of [shouldRemoveImageCallback] customization: + /// ```dart + /// shouldRemoveImageFromEditor: (imageFile) async { + /// // Show a confirmation dialog before removing the image + /// final isShouldRemove = await showYesCancelDialog( + /// context: context, + /// options: const YesOrCancelDialogOptions( + /// title: 'Deleting an image', + /// message: 'Are you sure you want' ' to delete this + /// image from the editor?', + /// ), + /// ); + /// + /// // Return `true` to allow image removal if the user confirms, otherwise + /// `false` + /// return isShouldRemove; + /// } + /// ``` + /// + final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback; + + /// [imageProviderBuilder] if you want to use custom image provider, please + /// pass a value to this property + /// By default we will use [NetworkImage] provider if the image url/path + /// is using http/https, if not then we will use [FileImage] provider + /// If you ovveride this make sure to handle the case where if the [imageUrl] + /// is in the local storage or it does exists in the system file + /// or use the same way we did it + /// + /// Example of [imageProviderBuilder] customization: + /// ```dart + /// imageProviderBuilder: (imageUrl) async { + /// // Example of using cached_network_image package + /// // Don't forgot to check if that image is local or network one + /// return CachedNetworkImageProvider(imageUrl); + /// } + /// ``` + /// + final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; + + /// [imageErrorWidgetBuilder] if you want to show a custom widget based on the + /// exception that happen while loading the image, if it network image or + /// local one, and it will get called on all the images even in the photo + /// preview widget and not just in the quill editor + /// by default the default error from flutter framework will thrown + /// + final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; + + /// [forceUseMobileOptionMenuForImageClick] is a boolean + /// flag that, when set to `true`, + /// enforces the use of the mobile-specific option menu for image clicks in + /// other platforms like desktop, this option doesn't affect mobile. it will + /// not affect web + /// This option + /// can be used to override the default behavior based on the platform. + /// + final bool forceUseMobileOptionMenuForImageClick; + + static ImageEmbedBuilderOnRemovedCallback get defaultOnImageRemovedCallback { + return (imageFile) async { + final mobile = isMobile(); + // If the platform is not mobile, return void; + // Since the mobile OS gives us a copy of the image + + // Note: We should remove the image on Flutter web + // since the behavior is similar to how it is on mobile, + // but since this builder is not for web, we will ignore it + if (!mobile) { + return; + } + + // On mobile OS (Android, iOS), the system will not give us + // direct access to the image; instead, + // it will give us the image + // in the temp directory of the application. So, we want to + // remove it when we no longer need it. + + // but on desktop we don't want to touch user files + // especially on macOS, where we can't even delete + // it without + // permission + + final isFileExists = await imageFile.exists(); + if (isFileExists) { + await imageFile.delete(); + } + }; + } +} + +@immutable +class QuillEditorWebImageEmbedConfigurations { + const QuillEditorWebImageEmbedConfigurations(); +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/video.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/video.dart new file mode 100644 index 00000000..53039dee --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/video.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart' show GlobalKey; +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorVideoEmbedConfigurations { + const QuillEditorVideoEmbedConfigurations({ + this.onVideoInit, + }); + + /// [onVideoInit] is a callback function that gets triggered when + /// a video is initialized. + /// You can use this to perform actions or setup configurations related + /// to video embedding. + /// + /// + /// Example usage: + /// ```dart + /// onVideoInit: (videoContainerKey) { + /// // Custom video initialization logic + /// }, + /// // Customize other callback functions as needed + /// ``` + final void Function(GlobalKey videoContainerKey)? onVideoInit; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart new file mode 100644 index 00000000..0d2149c6 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart @@ -0,0 +1,6 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorWebViewEmbedConfigurations { + const QuillEditorWebViewEmbedConfigurations(); +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart new file mode 100644 index 00000000..eef8d4dd --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart @@ -0,0 +1,49 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types.dart'; + +class QuillToolbarCameraButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarCameraButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> { + const QuillToolbarCameraButtonOptions({ + required this.onImagePickCallback, + required this.onVideoPickCallback, + this.webImagePickImpl, + this.webVideoPickImpl, + this.filePickImpl, + this.cameraPickSettingSelector, + this.iconSize, + this.fillColor, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final OnImagePickCallback onImagePickCallback; + + final OnVideoPickCallback onVideoPickCallback; + + final WebImagePickImpl? webImagePickImpl; + + final WebVideoPickImpl? webVideoPickImpl; + + final FilePickImpl? filePickImpl; + + final MediaPickSettingSelector? cameraPickSettingSelector; + + final double? iconSize; + + final Color? fillColor; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart new file mode 100644 index 00000000..7d716251 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart @@ -0,0 +1,28 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +class QuillToolbarFormulaButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarFormulaButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarFormulaButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarFormulaButtonOptions, QuillToolbarFormulaButtonExtraOptions> { + const QuillToolbarFormulaButtonOptions({ + super.tooltip, + super.iconData, + super.iconTheme, + super.afterButtonPressed, + super.childBuilder, + this.fillColor, + this.iconSize, + }); + + final Color? fillColor; + + final double? iconSize; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart new file mode 100644 index 00000000..98bbb265 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart @@ -0,0 +1,53 @@ +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.dart'; + +class QuillToolbarImageButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarImageButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +@immutable +class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarImageButtonOptions, QuillToolbarImageButtonExtraOptions> { + const QuillToolbarImageButtonOptions({ + super.iconData, + super.controller, + this.iconSize, + + /// specifies the tooltip text for the image button. + super.tooltip, + super.afterButtonPressed, + super.childBuilder, + super.iconTheme, + this.fillColor, + this.onImagePickCallback, + this.filePickImpl, + this.webImagePickImpl, + this.mediaPickSettingSelector, + this.dialogTheme, + this.linkRegExp, + }); + + final double? iconSize; + final Color? fillColor; + + final OnImagePickCallback? onImagePickCallback; + + final WebImagePickImpl? webImagePickImpl; + + final FilePickImpl? filePickImpl; + + final MediaPickSettingSelector? mediaPickSettingSelector; + + final QuillDialogTheme? dialogTheme; + + /// [imageLinkRegExp] is a regular expression to identify image links. + final RegExp? linkRegExp; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart new file mode 100644 index 00000000..06001e2d --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart @@ -0,0 +1,84 @@ +import 'package:flutter/widgets.dart' show AutovalidateMode; +import 'package:flutter/widgets.dart' show Color, Size; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types.dart'; + +class QuillToolbarMediaButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarMediaButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarMediaButtonOptions, QuillToolbarMediaButtonExtraOptions> { + const QuillToolbarMediaButtonOptions({ + required this.type, + required this.onMediaPickedCallback, + required this.onImagePickCallback, + required this.onVideoPickCallback, + this.dialogBarrierColor, + this.mediaFilePicker, + this.childrenSpacing = 16.0, + this.autovalidateMode = AutovalidateMode.disabled, + this.iconSize, + this.fillColor, + this.dialogTheme, + this.labelText, + this.hintText, + this.submitButtonText, + this.submitButtonSize, + this.galleryButtonText, + this.linkButtonText, + this.validationMessage, + this.filePickImpl, + this.webImagePickImpl, + this.webVideoPickImpl, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final double? iconSize; + final Color? fillColor; + final QuillMediaType type; + final QuillDialogTheme? dialogTheme; + final MediaFilePicker? mediaFilePicker; + final MediaPickedCallback? onMediaPickedCallback; + final Color? dialogBarrierColor; + + /// 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; + final OnImagePickCallback onImagePickCallback; + final FilePickImpl? filePickImpl; + final WebImagePickImpl? webImagePickImpl; + final OnVideoPickCallback onVideoPickCallback; + final WebVideoPickImpl? webVideoPickImpl; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart new file mode 100644 index 00000000..12995fda --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart @@ -0,0 +1,47 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types.dart'; + +class QuillToolbarVideoButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarVideoButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarVideoButtonOptions, QuillToolbarVideoButtonExtraOptions> { + const QuillToolbarVideoButtonOptions({ + this.linkRegExp, + this.dialogTheme, + this.onVideoPickCallback, + this.webVideoPickImpl, + this.filePickImpl, + this.mediaPickSettingSelector, + this.fillColor, + this.iconSize, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final RegExp? linkRegExp; + final QuillDialogTheme? dialogTheme; + final OnVideoPickCallback? onVideoPickCallback; + + final WebVideoPickImpl? webVideoPickImpl; + + final FilePickImpl? filePickImpl; + + final MediaPickSettingSelector? mediaPickSettingSelector; + + final Color? fillColor; + + final double? iconSize; +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart deleted file mode 100644 index baaf9ebd..00000000 --- a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart +++ /dev/null @@ -1,23 +0,0 @@ -// 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/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 7e2259e5..00ad2745 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,8 +1,15 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.5.1 -homepage: https://bulletjournal.us/home/index.html -repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions +version: 0.6.0-dev.1 +homepage: https://github.com/singerdmx/flutter-quill +repository: https://github.com/singerdmx/flutter-quill + +topics: + - ui + - widgets + - widget + - rich-text-editor + platforms: android: ios: @@ -12,31 +19,37 @@ platforms: windows: environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: '>=3.1.3 <4.0.0' + flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - flutter_quill: ^7.8.0 - # In case you are working on changes for both libraries, - # flutter_quill: - # path: ../ + flutter_quill: ^8.1.6 http: ^1.1.0 - image_picker: ">=1.0.4" photo_view: ^0.14.0 - video_player: ^2.7.2 + video_player: ^2.8.1 youtube_player_flutter: ^8.1.2 - math_keyboard: ">=0.2.1" + flutter_inappwebview: ^5.8.0 universal_html: ^2.2.4 - gal: ^2.1.2 + path: ^1.8.3 + image_picker: ^1.0.4 + math_keyboard: ^0.2.1 + url_launcher: ^6.2.1 + meta: ^1.9.1 + +# In case you are working on changes for both libraries, please uncomment this section +# dependency_overrides: +# flutter_quill: +# path: ../ dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 - + flutter_lints: ^3.0.0 + flutter: + uses-material-design: true \ No newline at end of file