diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index efc3a6ab..78e1bc77 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -4,13 +4,14 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_quill/flutter_quill.dart'; import 'package:meta/meta.dart' show immutable; +import 'logic/models/config/configurations.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/image_button/image_button.dart'; import 'presentation/embeds/toolbar/media_button.dart'; import 'presentation/embeds/toolbar/video_button.dart'; import 'presentation/models/config/editor/image.dart'; @@ -28,7 +29,7 @@ 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/image_button/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'; @@ -83,19 +84,7 @@ class FlutterQuillEmbeds { return [ if (imageEmbedConfigurations != null) QuillEditorImageEmbedBuilder( - configurations: QuillEditorImageEmbedConfigurations( - imageErrorWidgetBuilder: - imageEmbedConfigurations.imageErrorWidgetBuilder, - imageProviderBuilder: imageEmbedConfigurations.imageProviderBuilder, - forceUseMobileOptionMenuForImageClick: - imageEmbedConfigurations.forceUseMobileOptionMenuForImageClick, - onImageRemovedCallback: - imageEmbedConfigurations.onImageRemovedCallback ?? - QuillEditorImageEmbedConfigurations - .defaultOnImageRemovedCallback, - shouldRemoveImageCallback: - imageEmbedConfigurations.shouldRemoveImageCallback, - ), + configurations: imageEmbedConfigurations, ), if (videoEmbedConfigurations != null) QuillEditorVideoEmbedBuilder( @@ -115,6 +104,8 @@ class FlutterQuillEmbeds { /// images on the web. /// static List editorsWebBuilders({ + QuillSharedExtensionsConfigurations sharedExtensionsConfigurations = + const QuillSharedExtensionsConfigurations(), QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations = const QuillEditorWebImageEmbedConfigurations(), }) { diff --git a/flutter_quill_extensions/lib/logic/extensions/controller.dart b/flutter_quill_extensions/lib/logic/extensions/controller.dart index 3fe39ac2..856b87b7 100644 --- a/flutter_quill_extensions/lib/logic/extensions/controller.dart +++ b/flutter_quill_extensions/lib/logic/extensions/controller.dart @@ -28,7 +28,7 @@ extension QuillControllerExt on QuillController { required String imageUrl, }) { this - ..skipRequestKeyboard = true + ..skipRequestKeyboard = skipRequestKeyboard ..replaceText( index, length, diff --git a/flutter_quill_extensions/lib/logic/models/config/configurations.dart b/flutter_quill_extensions/lib/logic/models/config/configurations.dart new file mode 100644 index 00000000..307be978 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/models/config/configurations.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../services/image_picker/s_image_picker.dart'; +import '../../services/image_saver/s_image_saver.dart'; + +@immutable +class QuillSharedExtensionsConfigurations { + const QuillSharedExtensionsConfigurations({ + ImagePickerService? imagePickerService, + ImageSaverService? imageSaverService, + }) : _imagePickerService = imagePickerService, + _imageSaverService = imageSaverService; + + /// Get the instance from the widget tree in [QuillSharedConfigurations] + /// if it doesn't exists, we will create new one with default options + factory QuillSharedExtensionsConfigurations.get({ + required BuildContext context, + }) { + final quillSharedExtensionsConfigurations = + context.requireQuillSharedConfigurations.extraConfigurations[key]; + if (quillSharedExtensionsConfigurations != null) { + if (quillSharedExtensionsConfigurations + is! QuillSharedExtensionsConfigurations) { + throw ArgumentError( + 'The value of key `$key` should be of type ' + 'QuillSharedExtensionsConfigurations', + ); + } + return quillSharedExtensionsConfigurations; + } + return const QuillSharedExtensionsConfigurations(); + } + + static const String key = 'quillSharedExtensionsConfigurations'; + + /// Default to [ImagePickerService.defaultImpl] + final ImagePickerService? _imagePickerService; + + ImagePickerService get imagePickerService { + return _imagePickerService ?? ImagePickerService.defaultImpl(); + } + + /// Default to [ImageSaverService.defaultImpl] + final ImageSaverService? _imageSaverService; + + ImageSaverService get imageSaverService { + return _imageSaverService ?? ImageSaverService.defaultImpl(); + } +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart b/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart new file mode 100644 index 00000000..acecbacf --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart @@ -0,0 +1,20 @@ +/// Specifies the source where the picked image should come from. +enum ImageSource { + /// Opens up the device camera, letting the user to take a new picture. + camera, + + /// Opens the user's photo gallery. + gallery, +} + +enum CameraDevice { + /// Use the rear camera. + /// + /// In most of the cases, it is the default configuration. + rear, + + /// Use the front camera. + /// + /// Supported on all iPhones/iPads and some Android devices. + front, +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart new file mode 100644 index 00000000..b79d816a --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart @@ -0,0 +1,30 @@ +import 'package:cross_file/cross_file.dart' show XFile; + +import 'image_options.dart'; + +export 'package:cross_file/cross_file.dart' show XFile; + +export 'image_options.dart'; + +abstract class ImagePickerInterface { + const ImagePickerInterface(); + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }); + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }); + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }); +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart new file mode 100644 index 00000000..e009648d --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart @@ -0,0 +1,80 @@ +import 'package:image_picker/image_picker.dart' as package + show ImagePicker, ImageSource, CameraDevice; + +import '../image_picker.dart'; + +class ImagePickerPackageImpl extends ImagePickerInterface { + const ImagePickerPackageImpl(); + package.ImagePicker get _picker { + return package.ImagePicker(); + } + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }) { + return _picker.pickImage( + source: source.toImagePickerPackage(), + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice.toImagePickerPackage(), + requestFullMetadata: requestFullMetadata, + ); + } + + @override + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) { + return _picker.pickMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _picker.pickVideo( + source: source.toImagePickerPackage(), + preferredCameraDevice: preferredCameraDevice.toImagePickerPackage(), + maxDuration: maxDuration, + ); + } +} + +extension ImageSoureceExt on ImageSource { + package.ImageSource toImagePickerPackage() { + switch (this) { + case ImageSource.camera: + return package.ImageSource.camera; + case ImageSource.gallery: + return package.ImageSource.gallery; + } + } +} + +extension CameraDeviceExt on CameraDevice { + package.CameraDevice toImagePickerPackage() { + switch (this) { + case CameraDevice.rear: + return package.CameraDevice.rear; + case CameraDevice.front: + return package.CameraDevice.front; + } + } +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart new file mode 100644 index 00000000..9b9a43e4 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart @@ -0,0 +1,60 @@ +import 'image_picker.dart'; +import 'packages/image_picker.dart'; + +class ImagePickerService extends ImagePickerInterface { + const ImagePickerService( + this._impl, + ); + + factory ImagePickerService.imagePickerPackage() => const ImagePickerService( + ImagePickerPackageImpl(), + ); + + factory ImagePickerService.defaultImpl() => + ImagePickerService.imagePickerPackage(); + + final ImagePickerInterface _impl; + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }) => + _impl.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ); + + @override + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) => + _impl.pickMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) => + _impl.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); +} diff --git a/flutter_quill_extensions/lib/logic/services/exceptions.dart b/flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart similarity index 100% rename from flutter_quill_extensions/lib/logic/services/exceptions.dart rename to flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart diff --git a/flutter_quill_extensions/lib/logic/services/image_saver.dart b/flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart similarity index 100% rename from flutter_quill_extensions/lib/logic/services/image_saver.dart rename to flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart diff --git a/flutter_quill_extensions/lib/logic/services/packages/gal.dart b/flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart similarity index 100% rename from flutter_quill_extensions/lib/logic/services/packages/gal.dart rename to flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart diff --git a/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart b/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart new file mode 100644 index 00000000..f11587fd --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart @@ -0,0 +1,30 @@ +// 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 _impl; + const ImageSaverService(this._impl); + + factory ImageSaverService.galPackage() => ImageSaverService( + ImageSaverGalImpl(), + ); + + factory ImageSaverService.defaultImpl() => ImageSaverService.galPackage(); + + @override + Future hasAccess({bool toAlbum = false}) => + _impl.hasAccess(toAlbum: toAlbum); + + @override + Future requestAccess({bool toAlbum = false}) => + _impl.requestAccess(toAlbum: toAlbum); + + @override + Future saveImageFromNetwork(Uri imageUrl) => + _impl.saveImageFromNetwork(imageUrl); + + @override + Future saveLocalImage(String imageUrl) => + _impl.saveLocalImage(imageUrl); +} diff --git a/flutter_quill_extensions/lib/logic/services/s_image_saver.dart b/flutter_quill_extensions/lib/logic/services/s_image_saver.dart deleted file mode 100644 index 487e06ac..00000000 --- a/flutter_quill_extensions/lib/logic/services/s_image_saver.dart +++ /dev/null @@ -1,33 +0,0 @@ -// 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/presentation/embeds/editor/image/image.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart index c5170fa2..45fd1399 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart @@ -158,8 +158,7 @@ class QuillEditorImageEmbedBuilder extends EmbedBuilder { TextSelection.collapsed(offset: offset), ); // Call the post remove callback if set - await configurations.onImageRemovedCallback - ?.call(imageFile); + await configurations.onImageRemovedCallback.call(imageFile); }, ); return Padding( @@ -270,7 +269,10 @@ Widget _menuOptionsForReadonlyImage({ final messenger = ScaffoldMessenger.of(context); Navigator.of(context).pop(); - final saveImageResult = await saveImage(imageUrl); + final saveImageResult = await saveImage( + imageUrl: imageUrl, + context: context, + ); final imageSavedSuccessfully = saveImageResult.isSuccess; messenger.clearSnackBars(); diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart index 16774b00..3a2e9330 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart @@ -4,28 +4,18 @@ import 'dart:typed_data'; import 'package:flutter/material.dart' show ImageErrorWidgetBuilder, BuildContext, ImageProvider; -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, -); +// typedef WebImagePickImpl = Future Function( +// OnImagePickCallback onImagePickCallback, +// ); typedef WebVideoPickImpl = Future Function( OnVideoPickCallback onImagePickCallback, ); -typedef MediaPickSettingSelector = Future Function( - BuildContext context); - -enum MediaPickSetting { - gallery, - link, - camera, - video, -} typedef MediaFileUrl = String; typedef MediaFilePicker = Future Function(QuillMediaType mediaType); diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart new file mode 100644 index 00000000..5f1bc001 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart @@ -0,0 +1,42 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import '../../../logic/extensions/controller.dart'; +import '../../../logic/services/image_picker/s_image_picker.dart'; + +/// When request picking an image, for example when the image button toolbar +/// clicked, it should be null in case the user didn't choose any image or +/// any other reasons, and it should be the image file path as string that is +/// existied in case the user picked the image successfully +/// +/// by default we already have a default implementation that show a dialog +/// request the source for picking the image, from gallery, link or camera +typedef OnRequestPickImage = Future Function( + BuildContext context, + ImagePickerService imagePickerService, +); + +/// When a new image picked this callback will called and you might want to +/// do some logic depending on your use case +typedef OnImagePickedCallback = Future Function( + String image, +); + +/// A callback will called when inserting a image in the editor +typedef OnImageInsertCallback = Future Function( + String image, + QuillController controller, +); + +OnImageInsertCallback defaultOnImageInsertCallback() { + return (imageUrl, controller) async { + controller + ..skipRequestKeyboard = true + ..insertImageBlock(imageUrl: imageUrl); + }; +} + +enum InsertImageSource { + gallery, + camera, + link, +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart index 690f0f66..25859832 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart @@ -6,8 +6,6 @@ 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({ @@ -55,10 +53,6 @@ class QuillToolbarCameraButton extends StatelessWidget { _onPressedHandler( context, controller, - onImagePickCallback: options.onImagePickCallback, - onVideoPickCallback: options.onVideoPickCallback, - filePickImpl: options.filePickImpl, - webImagePickImpl: options.webImagePickImpl, ); _afterButtonPressed(context); } @@ -117,12 +111,8 @@ class QuillToolbarCameraButton extends StatelessWidget { Future _onPressedHandler( BuildContext context, - QuillController controller, { - OnImagePickCallback? onImagePickCallback, - OnVideoPickCallback? onVideoPickCallback, - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - }) async { + QuillController controller, + ) async { if (onVideoPickCallback == null && onImagePickCallback == null) { throw ArgumentError( 'onImagePickCallback and onVideoPickCallback are both null', diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart similarity index 62% rename from flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart rename to flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart index 88b54ee2..fbc7d361 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart @@ -2,11 +2,13 @@ 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'; +import '../../../../logic/models/config/configurations.dart'; +import '../../../../logic/services/image_picker/image_picker.dart'; +import '../../../models/config/toolbar/buttons/image.dart'; +import '../../embed_types/image.dart'; +import '../utils/image_video_utils.dart'; +import 'select_image_source.dart'; class QuillToolbarImageButton extends StatelessWidget { const QuillToolbarImageButton({ @@ -71,14 +73,11 @@ class QuillToolbarImageButton extends StatelessWidget { 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, + imageButtonConfigurations: options.imageButtonConfigurations, ), QuillToolbarImageButtonExtraOptions( context: context, @@ -113,70 +112,60 @@ class QuillToolbarImageButton extends StatelessWidget { } Future _onPressedHandler(BuildContext context) async { - final onImagePickCallbackRef = options.onImagePickCallback; - if (onImagePickCallbackRef == null) { - await _typeLink(context); + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + final onRequestPickImage = + options.imageButtonConfigurations.onRequestPickImage; + if (onRequestPickImage != null) { + final imageUrl = await onRequestPickImage( + context, + imagePickerService, + ); + if (imageUrl != null) { + await options.imageButtonConfigurations + .onImageInsertCallback(imageUrl, controller); + } return; } - final selector = options.mediaPickSettingSelector ?? - ImageVideoUtils.selectMediaPickSetting; - final source = await selector(context); + final source = await showSelectImageSourceDialog( + context: context, + ); if (source == null) { return; } + final String? imageUrl; switch (source) { - case MediaPickSetting.gallery: - _pickImage(context); + case InsertImageSource.gallery: + imageUrl = (await imagePickerService.pickImage( + source: ImageSource.gallery, + )) + ?.path; break; - case MediaPickSetting.link: - await _typeLink(context); + case InsertImageSource.link: + imageUrl = await _typeLink(context); break; - case MediaPickSetting.camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallbackRef, - filePickImpl: options.filePickImpl, - webImagePickImpl: options.webImagePickImpl, - ); + case InsertImageSource.camera: + imageUrl = (await imagePickerService.pickImage( + source: ImageSource.camera, + )) + ?.path; break; - case MediaPickSetting.video: - throw ArgumentError( - 'Sorry but this is the Image button and not the video one', - ); + } + if (imageUrl != null && imageUrl.trim().isNotEmpty) { + await options.imageButtonConfigurations + .onImageInsertCallback(imageUrl, controller); } } - 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 { + Future _typeLink(BuildContext context) async { final value = await showDialog( context: context, - builder: (_) => LinkDialog( + builder: (_) => TypeLinkDialog( 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); - } + return value; } } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart new file mode 100644 index 00000000..ab18fb28 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import '../../embed_types/image.dart'; + +class SelectImageSourceDialog extends StatelessWidget { + const SelectImageSourceDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 230, + width: double.infinity, + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + title: const Text('Gallery'), + subtitle: const Text( + 'Pick a photo from your gallery', + ), + leading: const Icon(Icons.photo_sharp), + onTap: () => Navigator.of(context).pop(InsertImageSource.gallery), + ), + ListTile( + title: const Text('Camera'), + subtitle: const Text( + 'Take a photo using your phone camera', + ), + leading: const Icon(Icons.camera), + onTap: () => Navigator.of(context).pop(InsertImageSource.camera), + ), + ListTile( + title: const Text('Link'), + subtitle: const Text( + 'Paste a photo using https link', + ), + leading: const Icon(Icons.link), + onTap: () => Navigator.of(context).pop(InsertImageSource.link), + ), + ], + ), + ), + ); + } +} + +Future showSelectImageSourceDialog({ + required BuildContext context, +}) async { + final imageSource = await showModalBottomSheet( + showDragHandle: true, + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (context) => const SelectImageSourceDialog(), + ); + return imageSource; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart index 4b03d7d7..b4d6ae57 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart @@ -1,17 +1,15 @@ -import 'dart:io' show File; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/translations.dart'; -import 'package:image_picker/image_picker.dart'; -import '../../../../logic/extensions/controller.dart'; -import '../../embed_types.dart'; +enum LinkType { + video, + image, +} -class LinkDialog extends StatefulWidget { - const LinkDialog({ +class TypeLinkDialog extends StatefulWidget { + const TypeLinkDialog({ + required this.linkType, this.dialogTheme, this.link, this.linkRegExp, @@ -21,12 +19,13 @@ class LinkDialog extends StatefulWidget { final QuillDialogTheme? dialogTheme; final String? link; final RegExp? linkRegExp; + final LinkType linkType; @override - LinkDialogState createState() => LinkDialogState(); + TypeLinkDialogState createState() => TypeLinkDialogState(); } -class LinkDialogState extends State { +class TypeLinkDialogState extends State { late String _link; late TextEditingController _controller; late RegExp _linkRegExp; @@ -65,7 +64,9 @@ class LinkDialogState extends State { style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( labelText: 'Paste a link'.i18n, - hintText: 'Please enter a valid image url'.i18n, + hintText: widget.linkType == LinkType.image + ? 'Please enter a valid image url'.i18n + : 'Please enter a valid video url'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle, ), @@ -106,150 +107,152 @@ class LinkDialogState extends State { } } -class ImageVideoUtils { - static Future selectMediaPickSetting( - BuildContext context, - ) => - showDialog( - context: context, - builder: (ctx) => AlertDialog( - contentPadding: EdgeInsets.zero, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextButton.icon( - icon: const Icon( - Icons.collections, - color: Colors.orangeAccent, - ), - label: Text('Gallery'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), - ), - TextButton.icon( - icon: const Icon( - Icons.link, - color: Colors.cyanAccent, - ), - label: Text('Link'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), - ) - ], - ), - ), - ); - - /// For image picking logic - static Future handleImageButtonTap( - BuildContext context, - QuillController controller, - ImageSource imageSource, - OnImagePickCallback onImagePickCallback, { - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - }) async { - String? imageUrl; - if (kIsWeb) { - 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 { - assert(filePickImpl != null, 'Desktop must provide filePickImpl'); - imageUrl = - await _pickImageDesktop(context, filePickImpl!, onImagePickCallback); - } - - if (imageUrl == null) { - return; - } - - controller.insertImageBlock( - imageUrl: imageUrl, - ); - } +// @immutable +// class ImageVideoUtils { +// const ImageVideoUtils._(); +// static Future selectMediaPickSetting( +// BuildContext context, +// ) => +// showDialog( +// context: context, +// builder: (ctx) => AlertDialog( +// contentPadding: EdgeInsets.zero, +// content: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// TextButton.icon( +// icon: const Icon( +// Icons.collections, +// color: Colors.orangeAccent, +// ), +// label: Text('Gallery'.i18n), +// onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), +// ), +// TextButton.icon( +// icon: const Icon( +// Icons.link, +// color: Colors.cyanAccent, +// ), +// label: Text('Link'.i18n), +// onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), +// ) +// ], +// ), +// ), +// ); - static Future _pickImage( - ImageSource source, - OnImagePickCallback onImagePickCallback, - ) async { - final pickedFile = await ImagePicker().pickImage(source: source); - if (pickedFile == null) { - return null; - } +// /// For image picking logic +// static Future handleImageButtonTap( +// BuildContext context, +// QuillController controller, +// ImageSource imageSource, +// OnImagePickCallback onImagePickCallback, { +// FilePickImpl? filePickImpl, +// WebImagePickImpl? webImagePickImpl, +// }) async { +// String? imageUrl; +// if (kIsWeb) { +// 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 { +// assert(filePickImpl != null, 'Desktop must provide filePickImpl'); +// imageUrl = +// await _pickImageDesktop(context, filePickImpl!, onImagePickCallback); +// } - return onImagePickCallback(File(pickedFile.path)); - } +// if (imageUrl == null) { +// return; +// } - static Future _pickImageDesktop( - BuildContext context, - FilePickImpl filePickImpl, - OnImagePickCallback onImagePickCallback, - ) async { - final filePath = await filePickImpl(context); - if (filePath == null || filePath.isEmpty) return null; +// controller.insertImageBlock( +// imageUrl: imageUrl, +// ); +// } - final file = File(filePath); - return onImagePickCallback(file); - } +// static Future _pickImage( +// ImageSource source, +// OnImagePickCallback onImagePickCallback, +// ) async { +// final pickedFile = await ImagePicker().pickImage(source: source); +// if (pickedFile == null) { +// return null; +// } - /// For video picking logic - static Future handleVideoButtonTap( - BuildContext context, - QuillController controller, - ImageSource videoSource, - OnVideoPickCallback onVideoPickCallback, { - FilePickImpl? filePickImpl, - WebVideoPickImpl? webVideoPickImpl, - }) async { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - String? videoUrl; - if (kIsWeb) { - assert( - 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); - } else { - assert(filePickImpl != null, 'Desktop must provide filePickImpl'); - videoUrl = - await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback); - } - - if (videoUrl != null) { - controller.replaceText(index, length, BlockEmbed.video(videoUrl), null); - } - } +// return onImagePickCallback(File(pickedFile.path)); +// } - static Future _pickVideo( - ImageSource source, OnVideoPickCallback onVideoPickCallback) async { - final pickedFile = await ImagePicker().pickVideo(source: source); - if (pickedFile == null) { - return null; - } +// static Future _pickImageDesktop( +// BuildContext context, +// FilePickImpl filePickImpl, +// OnImagePickCallback onImagePickCallback, +// ) async { +// final filePath = await filePickImpl(context); +// if (filePath == null || filePath.isEmpty) return null; - return onVideoPickCallback(File(pickedFile.path)); - } +// final file = File(filePath); +// return onImagePickCallback(file); +// } - static Future _pickVideoDesktop( - BuildContext context, - FilePickImpl filePickImpl, - OnVideoPickCallback onVideoPickCallback) async { - final filePath = await filePickImpl(context); - if (filePath == null || filePath.isEmpty) return null; +// /// For video picking logic +// static Future handleVideoButtonTap( +// BuildContext context, +// QuillController controller, +// ImageSource videoSource, +// OnVideoPickCallback onVideoPickCallback, { +// FilePickImpl? filePickImpl, +// WebVideoPickImpl? webVideoPickImpl, +// }) async { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; - final file = File(filePath); - return onVideoPickCallback(file); - } -} +// String? videoUrl; +// if (kIsWeb) { +// assert( +// 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); +// } else { +// assert(filePickImpl != null, 'Desktop must provide filePickImpl'); +// videoUrl = +// await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback); +// } + +// if (videoUrl != null) { +// controller.replaceText(index, length, BlockEmbed.video(videoUrl), null); +// } +// } + +// static Future _pickVideo( +// ImageSource source, OnVideoPickCallback onVideoPickCallback) async { +// final pickedFile = await ImagePicker().pickVideo(source: source); +// if (pickedFile == null) { +// return null; +// } + +// return onVideoPickCallback(File(pickedFile.path)); +// } + +// static Future _pickVideoDesktop( +// BuildContext context, +// FilePickImpl filePickImpl, +// OnVideoPickCallback onVideoPickCallback) async { +// final filePath = await filePickImpl(context); +// if (filePath == null || filePath.isEmpty) return null; + +// final file = File(filePath); +// return onVideoPickCallback(file); +// } +// } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart index 5019ac6b..2bed465d 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart @@ -137,7 +137,7 @@ class QuillToolbarVideoButton extends StatelessWidget { Future _typeLink(BuildContext context) async { final value = await showDialog( context: context, - builder: (_) => LinkDialog( + builder: (_) => TypeLinkDialog( dialogTheme: options.dialogTheme, ), ); diff --git a/flutter_quill_extensions/lib/presentation/embeds/utils.dart b/flutter_quill_extensions/lib/presentation/embeds/utils.dart index c7adfc42..68f8c314 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/utils.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/utils.dart @@ -1,7 +1,10 @@ import 'dart:io' show File; import 'package:flutter/foundation.dart' show immutable; -import '../../logic/services/s_image_saver.dart'; +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import '../../logic/models/config/configurations.dart'; +import '../../logic/services/image_saver/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 @@ -48,14 +51,19 @@ class SaveImageResult { final SaveImageResultMethod method; } -Future saveImage(String imageUrl) async { - final imageSaverService = ImageSaverService.getInstance(); +Future saveImage({ + required String imageUrl, + required BuildContext context, +}) async { + final imageSaverService = + QuillSharedExtensionsConfigurations.get(context: context) + .imageSaverService; final imageFile = File(imageUrl); final hasPermission = await imageSaverService.hasAccess(); - final imageExistsLocally = await imageFile.exists(); if (!hasPermission) { await imageSaverService.requestAccess(); } + final imageExistsLocally = await imageFile.exists(); if (!imageExistsLocally) { try { await imageSaverService.saveImageFromNetwork( diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart index 5a30ab15..2b494128 100644 --- a/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/image.dart @@ -11,11 +11,11 @@ import '../../../embeds/embed_types.dart'; class QuillEditorImageEmbedConfigurations { const QuillEditorImageEmbedConfigurations({ this.forceUseMobileOptionMenuForImageClick = false, - this.onImageRemovedCallback, + ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, this.shouldRemoveImageCallback, this.imageProviderBuilder, this.imageErrorWidgetBuilder, - }); + }) : _onImageRemovedCallback = onImageRemovedCallback; /// [onImageRemovedCallback] is called when an image is /// removed from the editor. @@ -33,7 +33,17 @@ class QuillEditorImageEmbedConfigurations { /// } /// ``` /// - final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback; + /// Default value if the passed value is null: + /// [QuillEditorImageEmbedConfigurations.defaultOnImageRemovedCallback] + /// + /// so if you want to do nothing make sure to pass a empty callback + /// instead of passing null as value + final ImageEmbedBuilderOnRemovedCallback? _onImageRemovedCallback; + + ImageEmbedBuilderOnRemovedCallback get onImageRemovedCallback { + return _onImageRemovedCallback ?? + QuillEditorImageEmbedConfigurations.defaultOnImageRemovedCallback; + } /// [shouldRemoveImageCallback] is a callback /// function that is invoked when the @@ -128,6 +138,26 @@ class QuillEditorImageEmbedConfigurations { } }; } + + QuillEditorImageEmbedConfigurations copyWith({ + ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, + ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback, + ImageEmbedBuilderProviderBuilder? imageProviderBuilder, + ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, + bool? forceUseMobileOptionMenuForImageClick, + }) { + return QuillEditorImageEmbedConfigurations( + onImageRemovedCallback: onImageRemovedCallback ?? _onImageRemovedCallback, + shouldRemoveImageCallback: + shouldRemoveImageCallback ?? this.shouldRemoveImageCallback, + imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder, + imageErrorWidgetBuilder: + imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder, + forceUseMobileOptionMenuForImageClick: + forceUseMobileOptionMenuForImageClick ?? + this.forceUseMobileOptionMenuForImageClick, + ); + } } @immutable 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 index eef8d4dd..b1b5bd8e 100644 --- a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart @@ -2,6 +2,8 @@ import 'package:flutter/widgets.dart' show Color; import 'package:flutter_quill/flutter_quill.dart'; import '../../../../embeds/embed_types.dart'; +import '../../../../embeds/embed_types/image.dart'; +import 'image.dart'; class QuillToolbarCameraButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions { @@ -15,12 +17,9 @@ class QuillToolbarCameraButtonExtraOptions class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> { const QuillToolbarCameraButtonOptions({ - required this.onImagePickCallback, required this.onVideoPickCallback, - this.webImagePickImpl, + this.imageConfigurations = const QuillToolbarImageButtonConfigurations(), this.webVideoPickImpl, - this.filePickImpl, - this.cameraPickSettingSelector, this.iconSize, this.fillColor, super.iconData, @@ -31,18 +30,12 @@ class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< super.controller, }); - final OnImagePickCallback onImagePickCallback; + final QuillToolbarImageButtonConfigurations imageConfigurations; 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/image.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart index 98bbb265..d6195c4c 100644 --- a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart @@ -2,7 +2,9 @@ import 'package:flutter/widgets.dart' show Color; import 'package:flutter_quill/flutter_quill.dart'; import 'package:meta/meta.dart' show immutable; +import '../../../../../logic/extensions/controller.dart'; import '../../../../embeds/embed_types.dart'; +import '../../../../embeds/embed_types/image.dart'; class QuillToolbarImageButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions { @@ -27,27 +29,37 @@ class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions< super.childBuilder, super.iconTheme, this.fillColor, - this.onImagePickCallback, - this.filePickImpl, - this.webImagePickImpl, - this.mediaPickSettingSelector, this.dialogTheme, this.linkRegExp, + this.imageButtonConfigurations = + const QuillToolbarImageButtonConfigurations(), }); final double? iconSize; final Color? fillColor; - final OnImagePickCallback? onImagePickCallback; + final QuillDialogTheme? dialogTheme; - final WebImagePickImpl? webImagePickImpl; + /// [imageLinkRegExp] is a regular expression to identify image links. + final RegExp? linkRegExp; - final FilePickImpl? filePickImpl; + final QuillToolbarImageButtonConfigurations imageButtonConfigurations; +} - final MediaPickSettingSelector? mediaPickSettingSelector; +class QuillToolbarImageButtonConfigurations { + const QuillToolbarImageButtonConfigurations({ + this.onRequestPickImage, + this.onImagePickedCallback, + OnImageInsertCallback? onImageInsertCallback, + }) : _onImageInsertCallback = onImageInsertCallback; - final QuillDialogTheme? dialogTheme; + final OnRequestPickImage? onRequestPickImage; - /// [imageLinkRegExp] is a regular expression to identify image links. - final RegExp? linkRegExp; + final OnImagePickedCallback? onImagePickedCallback; + + final OnImageInsertCallback? _onImageInsertCallback; + + OnImageInsertCallback get onImageInsertCallback { + return _onImageInsertCallback ?? defaultOnImageInsertCallback(); + } } 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 index 06001e2d..e35d59e9 100644 --- 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 @@ -76,9 +76,7 @@ class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions< 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 index 12995fda..96f121ec 100644 --- a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart @@ -39,8 +39,6 @@ class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions< final FilePickImpl? filePickImpl; - final MediaPickSettingSelector? mediaPickSettingSelector; - final Color? fillColor; final double? iconSize; diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 66fd0b4b..7a82cfff 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -40,11 +40,12 @@ dependencies: math_keyboard: ^0.2.1 url_launcher: ^6.2.1 meta: ^1.9.1 + cross_file: ^0.3.3+6 # In case you are working on changes for both libraries, please uncomment this section -# dependency_overrides: -# flutter_quill: -# path: ../ +dependency_overrides: + flutter_quill: + path: ../ dev_dependencies: flutter_test: