diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f121a6..1153a751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [8.1.12] +- Add the option to add configurations for `fresh_quill_extensions` using `extraConfigurations` + ## [8.1.11] - Follow dart best practices by using `lints` and remove `pedantic` as well `platform` since they are not used - Fix text direction bug diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index b8fc1f1d..4a49f260 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -21,7 +21,9 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) NSPhotoLibraryUsageDescription - Need to save image + The app will use it to pick images + NSCameraUsageDescription + The app will use it to capture a images, record videos. LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 34567dbb..f5a4a4a5 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/embeds/embed_types/image.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'; @@ -274,8 +275,12 @@ class _HomePageState extends State { configurations: QuillToolbarConfigurations( embedButtons: FlutterQuillEmbeds.toolbarButtons( imageButtonOptions: QuillToolbarImageButtonOptions( - onImagePickCallback: _onImagePickCallback, - webImagePickImpl: _webImagePickImpl, + imageButtonConfigurations: QuillToolbarImageConfigurations( + onImageInsertedCallback: (image) async { + _onImagePickCallback(File(image)); + }, + ), + // webImagePickImpl: _webImagePickImpl, ), ), buttonOptions: QuillToolbarButtonOptions( @@ -290,11 +295,15 @@ class _HomePageState extends State { if (_isDesktop()) { return QuillToolbar( configurations: QuillToolbarConfigurations( - showDirection: true, embedButtons: FlutterQuillEmbeds.toolbarButtons( imageButtonOptions: QuillToolbarImageButtonOptions( - onImagePickCallback: _onImagePickCallback, - filePickImpl: openFileSystemPickerForDesktop, + imageButtonConfigurations: QuillToolbarImageConfigurations( + onImageInsertedCallback: (image) async { + _onImagePickCallback(File(image)); + }, + ), + // onImagePickCallback: _onImagePickCallback, + // filePickImpl: openFileSystemPickerForDesktop, ), ), showAlignmentButtons: true, @@ -311,18 +320,23 @@ class _HomePageState extends State { configurations: QuillToolbarConfigurations( embedButtons: FlutterQuillEmbeds.toolbarButtons( imageButtonOptions: QuillToolbarImageButtonOptions( + imageButtonConfigurations: QuillToolbarImageConfigurations( + onImageInsertedCallback: (image) async { + _onImagePickCallback(File(image)); + }, + ), // 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, + // 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, - ), + // videoButtonOptions: QuillToolbarVideoButtonOptions( + // onVideoPickCallback: _onVideoPickCallback, + // ), ), showAlignmentButtons: true, buttonOptions: QuillToolbarButtonOptions( @@ -414,19 +428,19 @@ class _HomePageState extends State { return copiedFile.path.toString(); } - Future _webImagePickImpl( - OnImagePickCallback onImagePickCallback) async { - final result = await FilePicker.platform.pickFiles(); - if (result == null) { - return null; - } + // Future _webImagePickImpl( + // OnImagePickCallback onImagePickCallback) async { + // final result = await FilePicker.platform.pickFiles(); + // if (result == null) { + // return null; + // } - // Take first, because we don't allow picking multiple files. - final fileName = result.files.first.name; - final file = File(fileName); + // // Take first, because we don't allow picking multiple files. + // final fileName = result.files.first.name; + // final file = File(fileName); - return onImagePickCallback(file); - } + // return onImagePickCallback(file); + // } // Renders the video picked by imagePicker from local file storage // You can also upload the picked video to any server (eg : AWS s3 @@ -439,53 +453,53 @@ class _HomePageState extends State { return copiedFile.path.toString(); } - // ignore: unused_element - 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), - label: const Text('Gallery'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), - ), - TextButton.icon( - icon: const Icon(Icons.link), - label: const Text('Link'), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), - ) - ], - ), - ), - ); - - // ignore: unused_element - Future _selectCameraPickSetting(BuildContext context) => - showDialog( - context: context, - builder: (ctx) => AlertDialog( - contentPadding: EdgeInsets.zero, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextButton.icon( - icon: const Icon(Icons.camera), - label: const Text('Capture a photo'), - 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), - ) - ], - ), - ), - ); + // // ignore: unused_element + // 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), + // label: const Text('Gallery'), + // onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), + // ), + // TextButton.icon( + // icon: const Icon(Icons.link), + // label: const Text('Link'), + // onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), + // ) + // ], + // ), + // ), + // ); + + // // ignore: unused_element + // Future _selectCameraPickSetting(BuildContext context) => + // showDialog( + // context: context, + // builder: (ctx) => AlertDialog( + // contentPadding: EdgeInsets.zero, + // content: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // TextButton.icon( + // icon: const Icon(Icons.camera), + // label: const Text('Capture a photo'), + // 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), + // ) + // ], + // ), + // ), + // ); Widget _buildMenuBar(BuildContext context) { final size = MediaQuery.sizeOf(context); diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 78c350fc..27c6ebb0 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -89,9 +89,10 @@ class _DemoScaffoldState extends State { return QuillToolbar( configurations: QuillToolbarConfigurations( embedButtons: FlutterQuillEmbeds.toolbarButtons( - imageButtonOptions: QuillToolbarImageButtonOptions( - filePickImpl: openFileSystemPickerForDesktop, - ), + // ignore: avoid_redundant_argument_values + imageButtonOptions: const QuillToolbarImageButtonOptions( + // filePickImpl: openFileSystemPickerForDesktop, + ), ), ), ); diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 78e1bc77..bb617431 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -9,11 +9,13 @@ 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/camera_button/camera_button.dart'; import 'presentation/embeds/toolbar/formula_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'; +// TODO: Temporary +// ignore: unused_import +import 'presentation/embeds/toolbar/media_button/media_button.dart'; +import 'presentation/embeds/toolbar/video_button/video_button.dart'; import 'presentation/models/config/editor/image.dart'; import 'presentation/models/config/editor/video.dart'; import 'presentation/models/config/editor/webview.dart'; @@ -27,12 +29,12 @@ 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/camera_button/camera_button.dart'; export 'presentation/embeds/toolbar/formula_button.dart'; export 'presentation/embeds/toolbar/image_button/image_button.dart'; -export 'presentation/embeds/toolbar/media_button.dart'; +export 'presentation/embeds/toolbar/media_button/media_button.dart'; export 'presentation/embeds/toolbar/utils/image_video_utils.dart'; -export 'presentation/embeds/toolbar/video_button.dart'; +export 'presentation/embeds/toolbar/video_button/video_button.dart'; export 'presentation/embeds/utils.dart'; export 'presentation/models/config/editor/image.dart'; export 'presentation/models/config/toolbar/buttons/image.dart'; @@ -173,12 +175,12 @@ class FlutterQuillEmbeds { controller: cameraButtonOptions.controller ?? controller, options: cameraButtonOptions, ), - if (mediaButtonOptions != null) - (controller, toolbarIconSize, iconTheme, dialogTheme) => - QuillToolbarMediaButton( - controller: mediaButtonOptions.controller ?? controller, - options: mediaButtonOptions, - ), + // if (mediaButtonOptions != null) + // (controller, toolbarIconSize, iconTheme, dialogTheme) => + // QuillToolbarMediaButton( + // controller: mediaButtonOptions.controller ?? controller, + // options: mediaButtonOptions, + // ), if (formulaButtonOptions != null && !kIsWeb) (controller, toolbarIconSize, iconTheme, dialogTheme) => QuillToolbarFormulaButton( diff --git a/flutter_quill_extensions/lib/logic/extensions/controller.dart b/flutter_quill_extensions/lib/logic/extensions/controller.dart index 856b87b7..2318e5f6 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 = skipRequestKeyboard + ..skipRequestKeyboard = true ..replaceText( index, length, @@ -36,4 +36,12 @@ extension QuillControllerExt on QuillController { null, ); } + + void insertVideoBlock({ + required String videoUrl, + }) { + this + ..skipRequestKeyboard = true + ..replaceText(index, length, BlockEmbed.video(videoUrl), null); + } } diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart new file mode 100644 index 00000000..7ce65321 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:meta/meta.dart' show immutable; + +import 'image.dart'; + +enum CameraAction { + video, + image, +} + +/// When the user click the camera button, should we take a photo or record +/// a video using the camera +/// +/// by default will show a dialog that ask the user which option he/she wants +typedef OnRequestCameraActionCallback = Future Function( + BuildContext context, +); + +@immutable +class QuillToolbarCameraConfigurations { + const QuillToolbarCameraConfigurations({ + this.onRequestCameraActionCallback, + OnImageInsertCallback? onImageInsertCallback, + this.onImageInsertedCallback, + }) : _onImageInsertCallback = onImageInsertCallback; + + final OnRequestCameraActionCallback? onRequestCameraActionCallback; + + final OnImageInsertedCallback? onImageInsertedCallback; + + final OnImageInsertCallback? _onImageInsertCallback; + + OnImageInsertCallback get onImageInsertCallback { + return _onImageInsertCallback ?? defaultOnImageInsertCallback(); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart index 5f1bc001..e191c129 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart @@ -1,12 +1,13 @@ import 'package:flutter/widgets.dart' show BuildContext; import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; import '../../../logic/extensions/controller.dart'; import '../../../logic/services/image_picker/s_image_picker.dart'; /// 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 +/// exists 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 @@ -15,13 +16,8 @@ typedef OnRequestPickImage = Future Function( 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 +/// it have the logic that will insert the image block using the controller typedef OnImageInsertCallback = Future Function( String image, QuillController controller, @@ -35,8 +31,35 @@ OnImageInsertCallback defaultOnImageInsertCallback() { }; } +/// When a new image picked this callback will called and you might want to +/// do some logic depending on your use case +typedef OnImageInsertedCallback = Future Function( + String image, +); + enum InsertImageSource { gallery, camera, link, } + +/// Configurations for dealing with images, on insert a image +/// on request picking a image +@immutable +class QuillToolbarImageConfigurations { + const QuillToolbarImageConfigurations({ + this.onRequestPickImage, + this.onImageInsertedCallback, + OnImageInsertCallback? onImageInsertCallback, + }) : _onImageInsertCallback = onImageInsertCallback; + + final OnRequestPickImage? onRequestPickImage; + + final OnImageInsertedCallback? onImageInsertedCallback; + + final OnImageInsertCallback? _onImageInsertCallback; + + OnImageInsertCallback get onImageInsertCallback { + return _onImageInsertCallback ?? defaultOnImageInsertCallback(); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart new file mode 100644 index 00000000..d69ab5e0 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart @@ -0,0 +1,66 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../logic/extensions/controller.dart'; +import '../../../logic/services/image_picker/s_image_picker.dart'; + +/// When request picking an video, for example when the video button toolbar +/// clicked, it should be null in case the user didn't choose any video or +/// any other reasons, and it should be the video file path as string that is +/// exists in case the user picked the video successfully +/// +/// by default we already have a default implementation that show a dialog +/// request the source for picking the video, from gallery, link or camera +typedef OnRequestPickVideo = Future Function( + BuildContext context, + ImagePickerService imagePickerService, +); + +/// A callback will called when inserting a video in the editor +/// it have the logic that will insert the video block using the controller +typedef OnVideoInsertCallback = Future Function( + String video, + QuillController controller, +); + +OnVideoInsertCallback defaultOnVideoInsertCallback() { + return (videoUrl, controller) async { + controller + ..skipRequestKeyboard = true + ..insertVideoBlock(videoUrl: videoUrl); + }; +} + +/// When a new video picked this callback will called and you might want to +/// do some logic depending on your use case +typedef OnVideoInsertedCallback = Future Function( + String video, +); + +enum InsertVideoSource { + gallery, + camera, + link, +} + +/// Configurations for dealing with videos, on insert a video +/// on request picking a video +@immutable +class QuillToolbarVideoConfigurations { + const QuillToolbarVideoConfigurations({ + this.onRequestPickVideo, + this.onVideoInsertedCallback, + OnVideoInsertCallback? onVideoInsertCallback, + }) : _onVideoInsertCallback = onVideoInsertCallback; + + final OnRequestPickVideo? onRequestPickVideo; + + final OnVideoInsertedCallback? onVideoInsertedCallback; + + final OnVideoInsertCallback? _onVideoInsertCallback; + + OnVideoInsertCallback get onVideoInsertCallback { + return _onVideoInsertCallback ?? defaultOnVideoInsertCallback(); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart similarity index 53% rename from flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart rename to flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart index 25859832..1324c936 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart @@ -3,9 +3,12 @@ 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 '../../../../logic/models/config/configurations.dart'; +import '../../../../logic/services/image_picker/image_options.dart'; +import '../../../models/config/toolbar/buttons/camera.dart'; +import '../../embed_types/camera.dart'; +import 'select_camera_action.dart'; class QuillToolbarCameraButton extends StatelessWidget { const QuillToolbarCameraButton({ @@ -70,17 +73,14 @@ class QuillToolbarCameraButton extends StatelessWidget { 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, + cameraConfigurations: options.cameraConfigurations, webVideoPickImpl: options.webVideoPickImpl, ), QuillToolbarCameraButtonExtraOptions( @@ -109,84 +109,92 @@ class QuillToolbarCameraButton extends StatelessWidget { ); } + Future _getCameraAction(BuildContext context) async { + final customCallback = + options.cameraConfigurations.onRequestCameraActionCallback; + if (customCallback != null) { + return await customCallback(context); + } + final cameraAction = await showDialog( + context: context, + builder: (ctx) => const SelectCameraActionDialog(), + ); + + return cameraAction; + } + Future _onPressedHandler( BuildContext context, QuillController controller, ) 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) { + // if (onVideoPickCallback == null && onImagePickCallback == null) { + // throw ArgumentError( + // 'onImagePickCallback and onVideoPickCallback are both null', + // ); + // } + + final cameraAction = await _getCameraAction(context); + + if (cameraAction == 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, + + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + + switch (cameraAction) { + case CameraAction.video: + final videoFile = await imagePickerService.pickVideo( + source: ImageSource.camera, ); - break; - case MediaPickSetting.gallery: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'gallery is not related to camera button', + if (videoFile == null) { + return; + } + // TODO: Implement this + case CameraAction.image: + final imageFile = await imagePickerService.pickImage( + source: ImageSource.camera, ); - case MediaPickSetting.link: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'link is not related to camera button', + if (imageFile == null) { + return; + } + options.cameraConfigurations.onImageInsertCallback( + imageFile.path, + controller, ); + await options.cameraConfigurations.onImageInsertedCallback + ?.call(imageFile.path); } + + // final file = await switch (cameraAction) { + // CameraAction.image => + // imagePickerService.pickImage(source: ImageSource.camera), + // CameraAction.video => + // imagePickerService.pickVideo(source: ImageSource.camera), + // }; + + // 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; + // } } } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart new file mode 100644 index 00000000..a79fa393 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/translations.dart'; + +import '../../embed_types/camera.dart'; + +class SelectCameraActionDialog extends StatelessWidget { + const SelectCameraActionDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton.icon( + icon: const Icon( + Icons.camera, + color: Colors.orangeAccent, + ), + label: Text('Photo'.i18n), + onPressed: () => Navigator.pop(context, CameraAction.image), + ), + TextButton.icon( + icon: const Icon( + Icons.video_call, + color: Colors.cyanAccent, + ), + label: Text('Video'.i18n), + onPressed: () => Navigator.pop(context, CameraAction.video), + ) + ], + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart index fbc7d361..7451a435 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart @@ -115,6 +115,7 @@ class QuillToolbarImageButton extends StatelessWidget { final imagePickerService = QuillSharedExtensionsConfigurations.get(context: context) .imagePickerService; + final onRequestPickImage = options.imageButtonConfigurations.onRequestPickImage; if (onRequestPickImage != null) { @@ -125,6 +126,8 @@ class QuillToolbarImageButton extends StatelessWidget { if (imageUrl != null) { await options.imageButtonConfigurations .onImageInsertCallback(imageUrl, controller); + await options.imageButtonConfigurations.onImageInsertedCallback + ?.call(imageUrl); } return; } @@ -134,27 +137,26 @@ class QuillToolbarImageButton extends StatelessWidget { if (source == null) { return; } - final String? imageUrl; - switch (source) { - case InsertImageSource.gallery: - imageUrl = (await imagePickerService.pickImage( + + final imageUrl = switch (source) { + InsertImageSource.gallery => (await imagePickerService.pickImage( source: ImageSource.gallery, )) - ?.path; - break; - case InsertImageSource.link: - imageUrl = await _typeLink(context); - break; - case InsertImageSource.camera: - imageUrl = (await imagePickerService.pickImage( + ?.path, + InsertImageSource.link => await _typeLink(context), + InsertImageSource.camera => (await imagePickerService.pickImage( source: ImageSource.camera, )) - ?.path; - break; + ?.path, + }; + if (imageUrl == null) { + return; } - if (imageUrl != null && imageUrl.trim().isNotEmpty) { + if (imageUrl.trim().isNotEmpty) { await options.imageButtonConfigurations .onImageInsertCallback(imageUrl, controller); + await options.imageButtonConfigurations.onImageInsertedCallback + ?.call(imageUrl); } } @@ -164,6 +166,7 @@ class QuillToolbarImageButton extends StatelessWidget { builder: (_) => TypeLinkDialog( dialogTheme: options.dialogTheme, linkRegExp: options.linkRegExp, + linkType: LinkType.image, ), ); 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 index ab18fb28..3c838934 100644 --- 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 @@ -32,7 +32,7 @@ class SelectImageSourceDialog extends StatelessWidget { ListTile( title: const Text('Link'), subtitle: const Text( - 'Paste a photo using https link', + 'Paste a photo using a link', ), leading: const Icon(Icons.link), onTap: () => Navigator.of(context).pop(InsertImageSource.link), diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart deleted file mode 100644 index 212bd39e..00000000 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart +++ /dev/null @@ -1,533 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -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 '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 QuillToolbarMediaButton extends StatelessWidget { - QuillToolbarMediaButton({ - required this.controller, - required this.options, - super.key, - }) : assert(options.type == QuillMediaType.image, - 'Video selection is not supported yet'); - - final QuillController controller; - final QuillToolbarMediaButtonOptions 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, 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.'); - } - } - - IconData _iconData(BuildContext context) { - return options.iconData ?? - baseButtonExtraOptions(context).iconData ?? - _defaultData.$1; - } - - String _tooltip(BuildContext context) { - return options.tooltip ?? - baseButtonExtraOptions(context).tooltip ?? - _defaultData.$2; - // ('Camera'.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; - 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 = - options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = options.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 { - if (options.onMediaPickedCallback == null) { - _inputLink(context); - return; - } - final mediaSource = await showDialog( - context: context, - builder: (_) => MediaSourceSelectorDialog( - dialogTheme: options.dialogTheme, - galleryButtonText: options.galleryButtonText, - linkButtonText: options.linkButtonText, - ), - ); - if (mediaSource == null) { - return; - } - switch (mediaSource) { - case MediaPickSetting.gallery: - await _pickImage(); - break; - case MediaPickSetting.link: - _inputLink(context); - break; - case MediaPickSetting.camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - options.onImagePickCallback, - filePickImpl: options.filePickImpl, - webImagePickImpl: options.webImagePickImpl, - ); - break; - case MediaPickSetting.video: - await ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.camera, - options.onVideoPickCallback, - filePickImpl: options.filePickImpl, - webVideoPickImpl: options.webVideoPickImpl, - ); - break; - } - } - - Future _pickImage() async { - if (!(kIsWeb || isMobile() || isDesktop())) { - throw UnsupportedError( - 'Unsupported target platform: ${defaultTargetPlatform.name}', - ); - } - - final mediaFileUrl = await _pickMediaFileUrl(); - - if (mediaFileUrl != null) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - controller.replaceText( - index, - length, - BlockEmbed.image(mediaFileUrl), - null, - ); - } - } - - Future _pickMediaFileUrl() async { - final mediaFile = await options.mediaFilePicker?.call(options.type); - return mediaFile != null - ? options.onMediaPickedCallback?.call(mediaFile) - : null; - } - - void _inputLink(BuildContext context) { - showDialog( - context: context, - builder: (_) => MediaLinkDialog( - 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); - } - - void _linkSubmitted(String? value) { - if (value != null && value.isNotEmpty) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - final data = options.type.isImage - ? BlockEmbed.image(value) - : BlockEmbed.video(value); - controller.replaceText(index, length, data, null); - } - } -} - -/// Provides a dialog for input link to media resource. -class MediaLinkDialog extends StatefulWidget { - const MediaLinkDialog({ - super.key, - this.link, - this.dialogTheme, - this.childrenSpacing = 16.0, - this.labelText, - this.hintText, - this.buttonText, - this.buttonSize, - this.autovalidateMode = AutovalidateMode.disabled, - this.validationMessage, - }) : assert(childrenSpacing > 0); - - final String? link; - final QuillDialogTheme? dialogTheme; - - /// The margin between child widgets in the dialog. - final double childrenSpacing; - - /// The text of label in link add mode. - final String? labelText; - - /// The hint text for link [TextField]. - final String? hintText; - - /// The text of the submit button. - final String? buttonText; - - /// The size of dialog buttons. - final Size? buttonSize; - - final AutovalidateMode autovalidateMode; - final String? validationMessage; - - @override - State createState() => _MediaLinkDialogState(); -} - -class _MediaLinkDialogState extends State { - final _linkFocus = FocusNode(); - final _linkController = TextEditingController(); - - @override - void dispose() { - _linkFocus.dispose(); - _linkController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final constraints = widget.dialogTheme?.linkDialogConstraints ?? - () { - final size = MediaQuery.sizeOf(context); - final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; - return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); - }(); - - final buttonStyle = widget.buttonSize != null - ? Theme.of(context) - .elevatedButtonTheme - .style - ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) - : widget.dialogTheme?.buttonStyle; - - final isWrappable = widget.dialogTheme?.isWrappable ?? false; - - final children = [ - Text(widget.labelText ?? 'Enter media'.i18n), - UtilityWidgets.maybeWidget( - enabled: !isWrappable, - wrapper: (child) => Expanded( - child: child, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), - child: TextFormField( - controller: _linkController, - focusNode: _linkFocus, - style: widget.dialogTheme?.inputTextStyle, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - labelStyle: widget.dialogTheme?.labelTextStyle, - hintText: widget.hintText, - ), - autofocus: true, - autovalidateMode: widget.autovalidateMode, - validator: _validateLink, - onChanged: _linkChanged, - ), - ), - ), - ElevatedButton( - onPressed: _canPress() ? _submitLink : null, - style: buttonStyle, - child: Text(widget.buttonText ?? 'Ok'.i18n), - ), - ]; - - return Dialog( - backgroundColor: widget.dialogTheme?.dialogBackgroundColor, - shape: widget.dialogTheme?.shape ?? - DialogTheme.of(context).shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - child: ConstrainedBox( - constraints: constraints, - child: Padding( - padding: - widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), - child: Form( - child: isWrappable - ? Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, - children: children, - ) - : Row( - children: children, - ), - ), - ), - ), - ); - } - - bool _canPress() => _validateLink(_linkController.text) == null; - - void _linkChanged(String value) { - setState(() { - _linkController.text = value; - }); - } - - void _submitLink() => Navigator.pop(context, _linkController.text); - - String? _validateLink(String? value) { - if ((value?.isEmpty ?? false) || - !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { - return widget.validationMessage ?? 'That is not a valid URL'; - } - - return null; - } -} - -/// Media souce selector. -class MediaSourceSelectorDialog extends StatelessWidget { - const MediaSourceSelectorDialog({ - super.key, - this.dialogTheme, - this.galleryButtonText, - this.linkButtonText, - }); - - final QuillDialogTheme? dialogTheme; - - /// The text of the gallery button [MediaSourceSelectorDialog]. - final String? galleryButtonText; - - /// The text of the link button [MediaSourceSelectorDialog]. - final String? linkButtonText; - - @override - Widget build(BuildContext context) { - final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? - () { - final size = MediaQuery.sizeOf(context); - double maxWidth, maxHeight; - if (kIsWeb) { - maxWidth = size.width / 7; - maxHeight = size.height / 7; - } else { - maxWidth = size.width - 80; - maxHeight = maxWidth / 2; - } - return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); - }(); - - final shape = dialogTheme?.shape ?? - DialogTheme.of(context).shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); - - return Dialog( - backgroundColor: dialogTheme?.dialogBackgroundColor, - shape: shape, - child: ConstrainedBox( - constraints: constraints, - child: Padding( - padding: dialogTheme?.mediaSelectorDialogPadding ?? - const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: TextButtonWithIcon( - icon: Icons.collections, - label: galleryButtonText ?? 'Gallery'.i18n, - onPressed: () => - Navigator.pop(context, MediaPickSetting.gallery), - ), - ), - const SizedBox(width: 10), - Expanded( - child: TextButtonWithIcon( - icon: Icons.link, - label: linkButtonText ?? 'Link'.i18n, - onPressed: () => - Navigator.pop(context, MediaPickSetting.link), - ), - ) - ], - ), - ), - ), - ); - } -} - -class TextButtonWithIcon extends StatelessWidget { - const TextButtonWithIcon({ - required this.label, - required this.icon, - required this.onPressed, - this.textStyle, - super.key, - }); - - final String label; - final IconData icon; - final VoidCallback onPressed; - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; - final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; - final buttonStyle = TextButtonTheme.of(context).style; - final shape = buttonStyle?.shape?.resolve({}) ?? - const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(4), - ), - ); - return Material( - shape: shape, - textStyle: textStyle ?? - theme.textButtonTheme.style?.textStyle?.resolve({}) ?? - theme.textTheme.labelLarge, - elevation: buttonStyle?.elevation?.resolve({}) ?? 0, - child: InkWell( - customBorder: shape, - onTap: onPressed, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon), - SizedBox(height: gap), - Flexible(child: Text(label)), - ], - ), - ), - ), - ); - } -} - -/// Default file picker. -// Future _defaultMediaPicker(QuillMediaType mediaType) async { -// final pickedFile = mediaType.isImage -// ? await ImagePicker().pickImage(source: ImageSource.gallery) -// : await ImagePicker().pickVideo(source: ImageSource.gallery); - -// if (pickedFile != null) { -// return QuillFile( -// name: pickedFile.name, -// path: pickedFile.path, -// bytes: await pickedFile.readAsBytes(), -// ); -// } - -// return null; -// } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart new file mode 100644 index 00000000..1d8e5fcf --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart @@ -0,0 +1,533 @@ +// // ignore_for_file: use_build_context_synchronously + +// import 'dart:math' as math; +// import 'dart:ui'; + +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_quill/extensions.dart'; +// import 'package:flutter_quill/flutter_quill.dart'; +// 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 '../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 QuillToolbarMediaButton extends StatelessWidget { +// QuillToolbarMediaButton({ +// required this.controller, +// required this.options, +// super.key, +// }) : assert(options.type == QuillMediaType.image, +// 'Video selection is not supported yet'); + +// final QuillController controller; +// final QuillToolbarMediaButtonOptions 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, 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.'); +// } +// } + +// IconData _iconData(BuildContext context) { +// return options.iconData ?? +// baseButtonExtraOptions(context).iconData ?? +// _defaultData.$1; +// } + +// String _tooltip(BuildContext context) { +// return options.tooltip ?? +// baseButtonExtraOptions(context).tooltip ?? +// _defaultData.$2; +// // ('Camera'.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; +// 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 = +// options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; +// final iconFillColor = options.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 { +// if (options.onMediaPickedCallback == null) { +// _inputLink(context); +// return; +// } +// final mediaSource = await showDialog( +// context: context, +// builder: (_) => MediaSourceSelectorDialog( +// dialogTheme: options.dialogTheme, +// galleryButtonText: options.galleryButtonText, +// linkButtonText: options.linkButtonText, +// ), +// ); +// if (mediaSource == null) { +// return; +// } +// switch (mediaSource) { +// case MediaPickSetting.gallery: +// await _pickImage(); +// break; +// case MediaPickSetting.link: +// _inputLink(context); +// break; +// case MediaPickSetting.camera: +// await ImageVideoUtils.handleImageButtonTap( +// context, +// controller, +// ImageSource.camera, +// options.onImagePickCallback, +// filePickImpl: options.filePickImpl, +// webImagePickImpl: options.webImagePickImpl, +// ); +// break; +// case MediaPickSetting.video: +// await ImageVideoUtils.handleVideoButtonTap( +// context, +// controller, +// ImageSource.camera, +// options.onVideoPickCallback, +// filePickImpl: options.filePickImpl, +// webVideoPickImpl: options.webVideoPickImpl, +// ); +// break; +// } +// } + +// Future _pickImage() async { +// if (!(kIsWeb || isMobile() || isDesktop())) { +// throw UnsupportedError( +// 'Unsupported target platform: ${defaultTargetPlatform.name}', +// ); +// } + +// final mediaFileUrl = await _pickMediaFileUrl(); + +// if (mediaFileUrl != null) { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; +// controller.replaceText( +// index, +// length, +// BlockEmbed.image(mediaFileUrl), +// null, +// ); +// } +// } + +// Future _pickMediaFileUrl() async { +// final mediaFile = await options.mediaFilePicker?.call(options.type); +// return mediaFile != null +// ? options.onMediaPickedCallback?.call(mediaFile) +// : null; +// } + +// void _inputLink(BuildContext context) { +// showDialog( +// context: context, +// builder: (_) => MediaLinkDialog( +// 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); +// } + +// void _linkSubmitted(String? value) { +// if (value != null && value.isNotEmpty) { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; +// final data = options.type.isImage +// ? BlockEmbed.image(value) +// : BlockEmbed.video(value); +// controller.replaceText(index, length, data, null); +// } +// } +// } + +// /// Provides a dialog for input link to media resource. +// class MediaLinkDialog extends StatefulWidget { +// const MediaLinkDialog({ +// super.key, +// this.link, +// this.dialogTheme, +// this.childrenSpacing = 16.0, +// this.labelText, +// this.hintText, +// this.buttonText, +// this.buttonSize, +// this.autovalidateMode = AutovalidateMode.disabled, +// this.validationMessage, +// }) : assert(childrenSpacing > 0); + +// final String? link; +// final QuillDialogTheme? dialogTheme; + +// /// The margin between child widgets in the dialog. +// final double childrenSpacing; + +// /// The text of label in link add mode. +// final String? labelText; + +// /// The hint text for link [TextField]. +// final String? hintText; + +// /// The text of the submit button. +// final String? buttonText; + +// /// The size of dialog buttons. +// final Size? buttonSize; + +// final AutovalidateMode autovalidateMode; +// final String? validationMessage; + +// @override +// State createState() => _MediaLinkDialogState(); +// } + +// class _MediaLinkDialogState extends State { +// final _linkFocus = FocusNode(); +// final _linkController = TextEditingController(); + +// @override +// void dispose() { +// _linkFocus.dispose(); +// _linkController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// final constraints = widget.dialogTheme?.linkDialogConstraints ?? +// () { +// final size = MediaQuery.sizeOf(context); +// final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; +// return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); +// }(); + +// final buttonStyle = widget.buttonSize != null +// ? Theme.of(context) +// .elevatedButtonTheme +// .style +// ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) +// : widget.dialogTheme?.buttonStyle; + +// final isWrappable = widget.dialogTheme?.isWrappable ?? false; + +// final children = [ +// Text(widget.labelText ?? 'Enter media'.i18n), +// UtilityWidgets.maybeWidget( +// enabled: !isWrappable, +// wrapper: (child) => Expanded( +// child: child, +// ), +// child: Padding( +// padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), +// child: TextFormField( +// controller: _linkController, +// focusNode: _linkFocus, +// style: widget.dialogTheme?.inputTextStyle, +// keyboardType: TextInputType.url, +// textInputAction: TextInputAction.done, +// decoration: InputDecoration( +// labelStyle: widget.dialogTheme?.labelTextStyle, +// hintText: widget.hintText, +// ), +// autofocus: true, +// autovalidateMode: widget.autovalidateMode, +// validator: _validateLink, +// onChanged: _linkChanged, +// ), +// ), +// ), +// ElevatedButton( +// onPressed: _canPress() ? _submitLink : null, +// style: buttonStyle, +// child: Text(widget.buttonText ?? 'Ok'.i18n), +// ), +// ]; + +// return Dialog( +// backgroundColor: widget.dialogTheme?.dialogBackgroundColor, +// shape: widget.dialogTheme?.shape ?? +// DialogTheme.of(context).shape ?? +// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), +// child: ConstrainedBox( +// constraints: constraints, +// child: Padding( +// padding: +// widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), +// child: Form( +// child: isWrappable +// ? Wrap( +// alignment: WrapAlignment.center, +// crossAxisAlignment: WrapCrossAlignment.center, +// runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, +// children: children, +// ) +// : Row( +// children: children, +// ), +// ), +// ), +// ), +// ); +// } + +// bool _canPress() => _validateLink(_linkController.text) == null; + +// void _linkChanged(String value) { +// setState(() { +// _linkController.text = value; +// }); +// } + +// void _submitLink() => Navigator.pop(context, _linkController.text); + +// String? _validateLink(String? value) { +// if ((value?.isEmpty ?? false) || +// !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { +// return widget.validationMessage ?? 'That is not a valid URL'; +// } + +// return null; +// } +// } + +// /// Media souce selector. +// class MediaSourceSelectorDialog extends StatelessWidget { +// const MediaSourceSelectorDialog({ +// super.key, +// this.dialogTheme, +// this.galleryButtonText, +// this.linkButtonText, +// }); + +// final QuillDialogTheme? dialogTheme; + +// /// The text of the gallery button [MediaSourceSelectorDialog]. +// final String? galleryButtonText; + +// /// The text of the link button [MediaSourceSelectorDialog]. +// final String? linkButtonText; + +// @override +// Widget build(BuildContext context) { +// final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? +// () { +// final size = MediaQuery.sizeOf(context); +// double maxWidth, maxHeight; +// if (kIsWeb) { +// maxWidth = size.width / 7; +// maxHeight = size.height / 7; +// } else { +// maxWidth = size.width - 80; +// maxHeight = maxWidth / 2; +// } +// return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); +// }(); + +// final shape = dialogTheme?.shape ?? +// DialogTheme.of(context).shape ?? +// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); + +// return Dialog( +// backgroundColor: dialogTheme?.dialogBackgroundColor, +// shape: shape, +// child: ConstrainedBox( +// constraints: constraints, +// child: Padding( +// padding: dialogTheme?.mediaSelectorDialogPadding ?? +// const EdgeInsets.all(16), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Expanded( +// child: TextButtonWithIcon( +// icon: Icons.collections, +// label: galleryButtonText ?? 'Gallery'.i18n, +// onPressed: () => +// Navigator.pop(context, MediaPickSetting.gallery), +// ), +// ), +// const SizedBox(width: 10), +// Expanded( +// child: TextButtonWithIcon( +// icon: Icons.link, +// label: linkButtonText ?? 'Link'.i18n, +// onPressed: () => +// Navigator.pop(context, MediaPickSetting.link), +// ), +// ) +// ], +// ), +// ), +// ), +// ); +// } +// } + +// class TextButtonWithIcon extends StatelessWidget { +// const TextButtonWithIcon({ +// required this.label, +// required this.icon, +// required this.onPressed, +// this.textStyle, +// super.key, +// }); + +// final String label; +// final IconData icon; +// final VoidCallback onPressed; +// final TextStyle? textStyle; + +// @override +// Widget build(BuildContext context) { +// final theme = Theme.of(context); +// final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; +// final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; +// final buttonStyle = TextButtonTheme.of(context).style; +// final shape = buttonStyle?.shape?.resolve({}) ?? +// const RoundedRectangleBorder( +// borderRadius: BorderRadius.all( +// Radius.circular(4), +// ), +// ); +// return Material( +// shape: shape, +// textStyle: textStyle ?? +// theme.textButtonTheme.style?.textStyle?.resolve({}) ?? +// theme.textTheme.labelLarge, +// elevation: buttonStyle?.elevation?.resolve({}) ?? 0, +// child: InkWell( +// customBorder: shape, +// onTap: onPressed, +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Icon(icon), +// SizedBox(height: gap), +// Flexible(child: Text(label)), +// ], +// ), +// ), +// ), +// ); +// } +// } + +// /// Default file picker. +// // Future _defaultMediaPicker(QuillMediaType mediaType) async { +// // final pickedFile = mediaType.isImage +// // ? await ImagePicker().pickImage(source: ImageSource.gallery) +// // : await ImagePicker().pickVideo(source: ImageSource.gallery); + +// // if (pickedFile != null) { +// // return QuillFile( +// // name: pickedFile.name, +// // path: pickedFile.path, +// // bytes: await pickedFile.readAsBytes(), +// // ); +// // } + +// // return null; +// // } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart new file mode 100644 index 00000000..7187f10b --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import '../../embed_types/video.dart'; + +class SelectVideoSourceDialog extends StatelessWidget { + const SelectVideoSourceDialog({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 video from your gallery', + ), + leading: const Icon(Icons.photo_sharp), + onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery), + ), + ListTile( + title: const Text('Camera'), + subtitle: const Text( + 'Record a video using your phone camera', + ), + leading: const Icon(Icons.camera), + onTap: () => Navigator.of(context).pop(InsertVideoSource.camera), + ), + ListTile( + title: const Text('Link'), + subtitle: const Text( + 'Paste a video using a link', + ), + leading: const Icon(Icons.link), + onTap: () => Navigator.of(context).pop(InsertVideoSource.link), + ), + ], + ), + ), + ); + } +} + +Future showSelectVideoSourceDialog({ + required BuildContext context, +}) async { + final imageSource = await showModalBottomSheet( + showDragHandle: true, + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (context) => const SelectVideoSourceDialog(), + ); + return imageSource; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart similarity index 62% rename from flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart rename to flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart index 2bed465d..2b9bace3 100644 --- a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart @@ -2,11 +2,14 @@ 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'; +import '../../../../logic/models/config/configurations.dart'; +import '../../../../logic/services/image_picker/image_options.dart'; +import '../../../models/config/toolbar/buttons/video.dart'; +import '../../embed_types.dart'; +import '../../embed_types/video.dart'; +import '../utils/image_video_utils.dart'; +import 'select_video_source.dart'; class QuillToolbarVideoButton extends StatelessWidget { const QuillToolbarVideoButton({ @@ -78,15 +81,13 @@ class QuillToolbarVideoButton extends StatelessWidget { 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, + videoConfigurations: options.videoConfigurations, ), QuillToolbarVideoButtonExtraOptions( context: context, @@ -109,39 +110,67 @@ class QuillToolbarVideoButton extends StatelessWidget { } 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); - } + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + + final onRequestPickVideo = options.videoConfigurations.onRequestPickVideo; + if (onRequestPickVideo != null) { + final videoUrl = await onRequestPickVideo(context, imagePickerService); + if (videoUrl != null) { + await options.videoConfigurations + .onVideoInsertCallback(videoUrl, controller); + await options.videoConfigurations.onVideoInsertedCallback + ?.call(videoUrl); } - } else { - await _typeLink(context); + return; } - } - void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.gallery, - options.onVideoPickCallback!, - filePickImpl: options.filePickImpl, - webVideoPickImpl: options.webVideoPickImpl, - ); + final imageSource = await showSelectVideoSourceDialog(context: context); + + if (imageSource == null) { + return; + } + + final videoUrl = switch (imageSource) { + InsertVideoSource.gallery => + (await imagePickerService.pickVideo(source: ImageSource.gallery))?.path, + InsertVideoSource.camera => + (await imagePickerService.pickVideo(source: ImageSource.camera))?.path, + InsertVideoSource.link => await _typeLink(context), + }; + if (videoUrl == null) { + return; + } + + if (videoUrl.trim().isNotEmpty) { + await options.videoConfigurations + .onVideoInsertCallback(videoUrl, controller); + await options.videoConfigurations.onVideoInsertedCallback?.call(videoUrl); + } + + // if (options.onVideoPickCallback != null) { + // final selector = options.mediaPickSettingSelector ?? + // ImageVideoUtils.selectMediaPickSetting; + // final source = await selector(context); + // if (source != null) { + // if (source == MediaPickSetting.gallery) { + // } else { + // await _typeLink(context); + // } + // } + // } else {} + } - Future _typeLink(BuildContext context) async { + Future _typeLink(BuildContext context) async { final value = await showDialog( context: context, builder: (_) => TypeLinkDialog( dialogTheme: options.dialogTheme, + linkType: LinkType.video, ), ); - _linkSubmitted(value); + return value; } void _linkSubmitted(String? value) { 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 b1b5bd8e..c685bb4a 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,8 +2,7 @@ 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'; +import '../../../../embeds/embed_types/camera.dart'; class QuillToolbarCameraButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions { @@ -18,7 +17,7 @@ class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> { const QuillToolbarCameraButtonOptions({ required this.onVideoPickCallback, - this.imageConfigurations = const QuillToolbarImageButtonConfigurations(), + this.cameraConfigurations = const QuillToolbarCameraConfigurations(), this.webVideoPickImpl, this.iconSize, this.fillColor, @@ -30,13 +29,13 @@ class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< super.controller, }); - final QuillToolbarImageButtonConfigurations imageConfigurations; + final double? iconSize; + + final Color? fillColor; final OnVideoPickCallback onVideoPickCallback; final WebVideoPickImpl? webVideoPickImpl; - final double? iconSize; - - final Color? fillColor; + final QuillToolbarCameraConfigurations cameraConfigurations; } 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 d6195c4c..b0084eeb 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,8 +2,6 @@ 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 @@ -31,8 +29,7 @@ class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions< this.fillColor, this.dialogTheme, this.linkRegExp, - this.imageButtonConfigurations = - const QuillToolbarImageButtonConfigurations(), + this.imageButtonConfigurations = const QuillToolbarImageConfigurations(), }); final double? iconSize; @@ -43,23 +40,5 @@ class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions< /// [imageLinkRegExp] is a regular expression to identify image links. final RegExp? linkRegExp; - final QuillToolbarImageButtonConfigurations imageButtonConfigurations; -} - -class QuillToolbarImageButtonConfigurations { - const QuillToolbarImageButtonConfigurations({ - this.onRequestPickImage, - this.onImagePickedCallback, - OnImageInsertCallback? onImageInsertCallback, - }) : _onImageInsertCallback = onImageInsertCallback; - - final OnRequestPickImage? onRequestPickImage; - - final OnImagePickedCallback? onImagePickedCallback; - - final OnImageInsertCallback? _onImageInsertCallback; - - OnImageInsertCallback get onImageInsertCallback { - return _onImageInsertCallback ?? defaultOnImageInsertCallback(); - } + final QuillToolbarImageConfigurations imageButtonConfigurations; } 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 e35d59e9..a1c6fca9 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 @@ -18,7 +18,6 @@ class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions< const QuillToolbarMediaButtonOptions({ required this.type, required this.onMediaPickedCallback, - required this.onImagePickCallback, required this.onVideoPickCallback, this.dialogBarrierColor, this.mediaFilePicker, @@ -35,7 +34,6 @@ class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions< this.linkButtonText, this.validationMessage, this.filePickImpl, - this.webImagePickImpl, this.webVideoPickImpl, super.iconData, super.afterButtonPressed, 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 96f121ec..4e030192 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 @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart' show Color; import 'package:flutter_quill/flutter_quill.dart'; import '../../../../embeds/embed_types.dart'; +import '../../../../embeds/embed_types/video.dart'; class QuillToolbarVideoButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions { @@ -17,10 +18,7 @@ class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions< const QuillToolbarVideoButtonOptions({ this.linkRegExp, this.dialogTheme, - this.onVideoPickCallback, this.webVideoPickImpl, - this.filePickImpl, - this.mediaPickSettingSelector, this.fillColor, this.iconSize, super.iconData, @@ -29,16 +27,15 @@ class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions< super.iconTheme, super.childBuilder, super.controller, + this.videoConfigurations = const QuillToolbarVideoConfigurations(), }); final RegExp? linkRegExp; final QuillDialogTheme? dialogTheme; - final OnVideoPickCallback? onVideoPickCallback; + final QuillToolbarVideoConfigurations videoConfigurations; final WebVideoPickImpl? webVideoPickImpl; - final FilePickImpl? filePickImpl; - final Color? fillColor; final double? iconSize; diff --git a/lib/src/models/config/shared_configurations.dart b/lib/src/models/config/shared_configurations.dart index 78dd6b06..98a87e03 100644 --- a/lib/src/models/config/shared_configurations.dart +++ b/lib/src/models/config/shared_configurations.dart @@ -18,6 +18,7 @@ class QuillSharedConfigurations extends Equatable { this.animationConfigurations = const QuillAnimationConfigurations( checkBoxPointItem: false, ), + this.extraConfigurations = const {}, }); // This is just example or showcase of this major update to make the library @@ -36,6 +37,9 @@ class QuillSharedConfigurations extends Equatable { /// To configure which animations you want to be enabled final QuillAnimationConfigurations animationConfigurations; + /// Store custom configurations in here and use it in the widget tree + final Map extraConfigurations; + @override List get props => [ dialogBarrierColor, diff --git a/pubspec.yaml b/pubspec.yaml index 5be31f2b..56015302 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. -version: 8.1.11 +version: 8.1.12 homepage: https://1o24bbs.com/c/bulletjournal/108 repository: https://github.com/singerdmx/flutter-quill