pull/1507/head
Ellet 1 year ago committed by X Code
parent e2cfa1d453
commit 11dd8e7d39
  1. 21
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  2. 2
      flutter_quill_extensions/lib/logic/extensions/controller.dart
  3. 51
      flutter_quill_extensions/lib/logic/models/config/configurations.dart
  4. 20
      flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart
  5. 30
      flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart
  6. 80
      flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart
  7. 60
      flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart
  8. 0
      flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart
  9. 0
      flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart
  10. 0
      flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart
  11. 30
      flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart
  12. 33
      flutter_quill_extensions/lib/logic/services/s_image_saver.dart
  13. 8
      flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart
  14. 16
      flutter_quill_extensions/lib/presentation/embeds/embed_types.dart
  15. 42
      flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart
  16. 14
      flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button.dart
  17. 99
      flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart
  18. 57
      flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart
  19. 303
      flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart
  20. 2
      flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button.dart
  21. 16
      flutter_quill_extensions/lib/presentation/embeds/utils.dart
  22. 36
      flutter_quill_extensions/lib/presentation/models/config/editor/image.dart
  23. 15
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart
  24. 34
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart
  25. 2
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart
  26. 2
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart
  27. 7
      flutter_quill_extensions/pubspec.yaml

@ -4,13 +4,14 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; 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.dart';
import 'presentation/embeds/editor/image/image_web.dart'; import 'presentation/embeds/editor/image/image_web.dart';
import 'presentation/embeds/editor/video.dart'; import 'presentation/embeds/editor/video.dart';
import 'presentation/embeds/editor/webview.dart'; import 'presentation/embeds/editor/webview.dart';
import 'presentation/embeds/toolbar/camera_button.dart'; import 'presentation/embeds/toolbar/camera_button.dart';
import 'presentation/embeds/toolbar/formula_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/media_button.dart';
import 'presentation/embeds/toolbar/video_button.dart'; import 'presentation/embeds/toolbar/video_button.dart';
import 'presentation/models/config/editor/image.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/embed_types.dart';
export 'presentation/embeds/toolbar/camera_button.dart'; export 'presentation/embeds/toolbar/camera_button.dart';
export 'presentation/embeds/toolbar/formula_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/media_button.dart';
export 'presentation/embeds/toolbar/utils/image_video_utils.dart'; export 'presentation/embeds/toolbar/utils/image_video_utils.dart';
export 'presentation/embeds/toolbar/video_button.dart'; export 'presentation/embeds/toolbar/video_button.dart';
@ -83,19 +84,7 @@ class FlutterQuillEmbeds {
return [ return [
if (imageEmbedConfigurations != null) if (imageEmbedConfigurations != null)
QuillEditorImageEmbedBuilder( QuillEditorImageEmbedBuilder(
configurations: QuillEditorImageEmbedConfigurations( configurations: imageEmbedConfigurations,
imageErrorWidgetBuilder:
imageEmbedConfigurations.imageErrorWidgetBuilder,
imageProviderBuilder: imageEmbedConfigurations.imageProviderBuilder,
forceUseMobileOptionMenuForImageClick:
imageEmbedConfigurations.forceUseMobileOptionMenuForImageClick,
onImageRemovedCallback:
imageEmbedConfigurations.onImageRemovedCallback ??
QuillEditorImageEmbedConfigurations
.defaultOnImageRemovedCallback,
shouldRemoveImageCallback:
imageEmbedConfigurations.shouldRemoveImageCallback,
),
), ),
if (videoEmbedConfigurations != null) if (videoEmbedConfigurations != null)
QuillEditorVideoEmbedBuilder( QuillEditorVideoEmbedBuilder(
@ -115,6 +104,8 @@ class FlutterQuillEmbeds {
/// images on the web. /// images on the web.
/// ///
static List<EmbedBuilder> editorsWebBuilders({ static List<EmbedBuilder> editorsWebBuilders({
QuillSharedExtensionsConfigurations sharedExtensionsConfigurations =
const QuillSharedExtensionsConfigurations(),
QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations = QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations =
const QuillEditorWebImageEmbedConfigurations(), const QuillEditorWebImageEmbedConfigurations(),
}) { }) {

@ -28,7 +28,7 @@ extension QuillControllerExt on QuillController {
required String imageUrl, required String imageUrl,
}) { }) {
this this
..skipRequestKeyboard = true ..skipRequestKeyboard = skipRequestKeyboard
..replaceText( ..replaceText(
index, index,
length, length,

@ -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();
}
}

@ -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,
}

@ -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<XFile?> pickImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
bool requestFullMetadata = true,
});
Future<XFile?> pickMedia({
double? maxWidth,
double? maxHeight,
int? imageQuality,
bool requestFullMetadata = true,
});
Future<XFile?> pickVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
});
}

@ -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<XFile?> 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<XFile?> pickMedia({
double? maxWidth,
double? maxHeight,
int? imageQuality,
bool requestFullMetadata = true,
}) {
return _picker.pickMedia(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
requestFullMetadata: requestFullMetadata,
);
}
@override
Future<XFile?> 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;
}
}
}

@ -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<XFile?> 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<XFile?> pickMedia({
double? maxWidth,
double? maxHeight,
int? imageQuality,
bool requestFullMetadata = true,
}) =>
_impl.pickMedia(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
requestFullMetadata: requestFullMetadata,
);
@override
Future<XFile?> pickVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) =>
_impl.pickVideo(
source: source,
preferredCameraDevice: preferredCameraDevice,
maxDuration: maxDuration,
);
}

@ -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<bool> hasAccess({bool toAlbum = false}) =>
_impl.hasAccess(toAlbum: toAlbum);
@override
Future<bool> requestAccess({bool toAlbum = false}) =>
_impl.requestAccess(toAlbum: toAlbum);
@override
Future<void> saveImageFromNetwork(Uri imageUrl) =>
_impl.saveImageFromNetwork(imageUrl);
@override
Future<void> saveLocalImage(String imageUrl) =>
_impl.saveLocalImage(imageUrl);
}

@ -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<bool> hasAccess({bool toAlbum = false}) =>
_provider.hasAccess(toAlbum: toAlbum);
@override
Future<bool> requestAccess({bool toAlbum = false}) =>
_provider.requestAccess(toAlbum: toAlbum);
@override
Future<void> saveImageFromNetwork(Uri imageUrl) =>
_provider.saveImageFromNetwork(imageUrl);
@override
Future<void> saveLocalImage(String imageUrl) =>
_provider.saveLocalImage(imageUrl);
}

@ -158,8 +158,7 @@ class QuillEditorImageEmbedBuilder extends EmbedBuilder {
TextSelection.collapsed(offset: offset), TextSelection.collapsed(offset: offset),
); );
// Call the post remove callback if set // Call the post remove callback if set
await configurations.onImageRemovedCallback await configurations.onImageRemovedCallback.call(imageFile);
?.call(imageFile);
}, },
); );
return Padding( return Padding(
@ -270,7 +269,10 @@ Widget _menuOptionsForReadonlyImage({
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop(); Navigator.of(context).pop();
final saveImageResult = await saveImage(imageUrl); final saveImageResult = await saveImage(
imageUrl: imageUrl,
context: context,
);
final imageSavedSuccessfully = saveImageResult.isSuccess; final imageSavedSuccessfully = saveImageResult.isSuccess;
messenger.clearSnackBars(); messenger.clearSnackBars();

@ -4,28 +4,18 @@ import 'dart:typed_data';
import 'package:flutter/material.dart' import 'package:flutter/material.dart'
show ImageErrorWidgetBuilder, BuildContext, ImageProvider; show ImageErrorWidgetBuilder, BuildContext, ImageProvider;
typedef OnImagePickCallback = Future<String?> Function(File file);
typedef OnVideoPickCallback = Future<String?> Function(File file); typedef OnVideoPickCallback = Future<String?> Function(File file);
/// [FilePickImpl] is an implementation for picking files. /// [FilePickImpl] is an implementation for picking files.
typedef FilePickImpl = Future<String?> Function(BuildContext context); typedef FilePickImpl = Future<String?> Function(BuildContext context);
/// [WebImagePickImpl] is an implementation for picking web images. /// [WebImagePickImpl] is an implementation for picking web images.
typedef WebImagePickImpl = Future<String?> Function( // typedef WebImagePickImpl = Future<String?> Function(
OnImagePickCallback onImagePickCallback, // OnImagePickCallback onImagePickCallback,
); // );
typedef WebVideoPickImpl = Future<String?> Function( typedef WebVideoPickImpl = Future<String?> Function(
OnVideoPickCallback onImagePickCallback, OnVideoPickCallback onImagePickCallback,
); );
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(
BuildContext context);
enum MediaPickSetting {
gallery,
link,
camera,
video,
}
typedef MediaFileUrl = String; typedef MediaFileUrl = String;
typedef MediaFilePicker = Future<QuillFile?> Function(QuillMediaType mediaType); typedef MediaFilePicker = Future<QuillFile?> Function(QuillMediaType mediaType);

@ -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<String?> 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<void> Function(
String image,
);
/// A callback will called when inserting a image in the editor
typedef OnImageInsertCallback = Future<void> Function(
String image,
QuillController controller,
);
OnImageInsertCallback defaultOnImageInsertCallback() {
return (imageUrl, controller) async {
controller
..skipRequestKeyboard = true
..insertImageBlock(imageUrl: imageUrl);
};
}
enum InsertImageSource {
gallery,
camera,
link,
}

@ -6,8 +6,6 @@ import 'package:flutter_quill/translations.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../../models/config/toolbar/buttons/camera.dart'; import '../../models/config/toolbar/buttons/camera.dart';
import '../embed_types.dart';
import 'utils/image_video_utils.dart';
class QuillToolbarCameraButton extends StatelessWidget { class QuillToolbarCameraButton extends StatelessWidget {
const QuillToolbarCameraButton({ const QuillToolbarCameraButton({
@ -55,10 +53,6 @@ class QuillToolbarCameraButton extends StatelessWidget {
_onPressedHandler( _onPressedHandler(
context, context,
controller, controller,
onImagePickCallback: options.onImagePickCallback,
onVideoPickCallback: options.onVideoPickCallback,
filePickImpl: options.filePickImpl,
webImagePickImpl: options.webImagePickImpl,
); );
_afterButtonPressed(context); _afterButtonPressed(context);
} }
@ -117,12 +111,8 @@ class QuillToolbarCameraButton extends StatelessWidget {
Future<void> _onPressedHandler( Future<void> _onPressedHandler(
BuildContext context, BuildContext context,
QuillController controller, { QuillController controller,
OnImagePickCallback? onImagePickCallback, ) async {
OnVideoPickCallback? onVideoPickCallback,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
}) async {
if (onVideoPickCallback == null && onImagePickCallback == null) { if (onVideoPickCallback == null && onImagePickCallback == null) {
throw ArgumentError( throw ArgumentError(
'onImagePickCallback and onVideoPickCallback are both null', 'onImagePickCallback and onVideoPickCallback are both null',

@ -2,11 +2,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/config/toolbar/buttons/image.dart'; import '../../../../logic/models/config/configurations.dart';
import '../embed_types.dart'; import '../../../../logic/services/image_picker/image_picker.dart';
import 'utils/image_video_utils.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 { class QuillToolbarImageButton extends StatelessWidget {
const QuillToolbarImageButton({ const QuillToolbarImageButton({
@ -71,14 +73,11 @@ class QuillToolbarImageButton extends StatelessWidget {
iconData: iconData, iconData: iconData,
iconSize: iconSize, iconSize: iconSize,
dialogTheme: options.dialogTheme, dialogTheme: options.dialogTheme,
filePickImpl: options.filePickImpl,
webImagePickImpl: options.webImagePickImpl,
fillColor: options.fillColor, fillColor: options.fillColor,
iconTheme: options.iconTheme, iconTheme: options.iconTheme,
linkRegExp: options.linkRegExp, linkRegExp: options.linkRegExp,
mediaPickSettingSelector: options.mediaPickSettingSelector,
onImagePickCallback: options.onImagePickCallback,
tooltip: options.tooltip, tooltip: options.tooltip,
imageButtonConfigurations: options.imageButtonConfigurations,
), ),
QuillToolbarImageButtonExtraOptions( QuillToolbarImageButtonExtraOptions(
context: context, context: context,
@ -113,70 +112,60 @@ class QuillToolbarImageButton extends StatelessWidget {
} }
Future<void> _onPressedHandler(BuildContext context) async { Future<void> _onPressedHandler(BuildContext context) async {
final onImagePickCallbackRef = options.onImagePickCallback; final imagePickerService =
if (onImagePickCallbackRef == null) { QuillSharedExtensionsConfigurations.get(context: context)
await _typeLink(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; return;
} }
final selector = options.mediaPickSettingSelector ?? final source = await showSelectImageSourceDialog(
ImageVideoUtils.selectMediaPickSetting; context: context,
final source = await selector(context); );
if (source == null) { if (source == null) {
return; return;
} }
final String? imageUrl;
switch (source) { switch (source) {
case MediaPickSetting.gallery: case InsertImageSource.gallery:
_pickImage(context); imageUrl = (await imagePickerService.pickImage(
source: ImageSource.gallery,
))
?.path;
break; break;
case MediaPickSetting.link: case InsertImageSource.link:
await _typeLink(context); imageUrl = await _typeLink(context);
break; break;
case MediaPickSetting.camera: case InsertImageSource.camera:
await ImageVideoUtils.handleImageButtonTap( imageUrl = (await imagePickerService.pickImage(
context, source: ImageSource.camera,
controller, ))
ImageSource.camera, ?.path;
onImagePickCallbackRef,
filePickImpl: options.filePickImpl,
webImagePickImpl: options.webImagePickImpl,
);
break; break;
case MediaPickSetting.video: }
throw ArgumentError( if (imageUrl != null && imageUrl.trim().isNotEmpty) {
'Sorry but this is the Image button and not the video one', await options.imageButtonConfigurations
); .onImageInsertCallback(imageUrl, controller);
} }
} }
void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( Future<String?> _typeLink(BuildContext context) async {
context,
controller,
ImageSource.gallery,
options.onImagePickCallback ??
(throw ArgumentError(
'onImagePickCallback should not be null',
)),
filePickImpl: options.filePickImpl,
webImagePickImpl: options.webImagePickImpl,
);
Future<void> _typeLink(BuildContext context) async {
final value = await showDialog<String>( final value = await showDialog<String>(
context: context, context: context,
builder: (_) => LinkDialog( builder: (_) => TypeLinkDialog(
dialogTheme: options.dialogTheme, dialogTheme: options.dialogTheme,
linkRegExp: options.linkRegExp, linkRegExp: options.linkRegExp,
), ),
); );
_linkSubmitted(value); return 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);
}
} }
} }

@ -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<InsertImageSource?> showSelectImageSourceDialog({
required BuildContext context,
}) async {
final imageSource = await showModalBottomSheet<InsertImageSource>(
showDragHandle: true,
context: context,
constraints: const BoxConstraints(maxWidth: 640),
builder: (context) => const SelectImageSourceDialog(),
);
return imageSource;
}

@ -1,17 +1,15 @@
import 'dart:io' show File;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import 'package:image_picker/image_picker.dart';
import '../../../../logic/extensions/controller.dart'; enum LinkType {
import '../../embed_types.dart'; video,
image,
}
class LinkDialog extends StatefulWidget { class TypeLinkDialog extends StatefulWidget {
const LinkDialog({ const TypeLinkDialog({
required this.linkType,
this.dialogTheme, this.dialogTheme,
this.link, this.link,
this.linkRegExp, this.linkRegExp,
@ -21,12 +19,13 @@ class LinkDialog extends StatefulWidget {
final QuillDialogTheme? dialogTheme; final QuillDialogTheme? dialogTheme;
final String? link; final String? link;
final RegExp? linkRegExp; final RegExp? linkRegExp;
final LinkType linkType;
@override @override
LinkDialogState createState() => LinkDialogState(); TypeLinkDialogState createState() => TypeLinkDialogState();
} }
class LinkDialogState extends State<LinkDialog> { class TypeLinkDialogState extends State<TypeLinkDialog> {
late String _link; late String _link;
late TextEditingController _controller; late TextEditingController _controller;
late RegExp _linkRegExp; late RegExp _linkRegExp;
@ -65,7 +64,9 @@ class LinkDialogState extends State<LinkDialog> {
style: widget.dialogTheme?.inputTextStyle, style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Paste a link'.i18n, 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, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
), ),
@ -106,150 +107,152 @@ class LinkDialogState extends State<LinkDialog> {
} }
} }
class ImageVideoUtils { // @immutable
static Future<MediaPickSetting?> selectMediaPickSetting( // class ImageVideoUtils {
BuildContext context, // const ImageVideoUtils._();
) => // static Future<MediaPickSetting?> selectMediaPickSetting(
showDialog<MediaPickSetting>( // BuildContext context,
context: context, // ) =>
builder: (ctx) => AlertDialog( // showDialog<MediaPickSetting>(
contentPadding: EdgeInsets.zero, // context: context,
content: Column( // builder: (ctx) => AlertDialog(
mainAxisSize: MainAxisSize.min, // contentPadding: EdgeInsets.zero,
children: [ // content: Column(
TextButton.icon( // mainAxisSize: MainAxisSize.min,
icon: const Icon( // children: [
Icons.collections, // TextButton.icon(
color: Colors.orangeAccent, // icon: const Icon(
), // Icons.collections,
label: Text('Gallery'.i18n), // color: Colors.orangeAccent,
onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery), // ),
), // label: Text('Gallery'.i18n),
TextButton.icon( // onPressed: () => Navigator.pop(ctx, MediaPickSetting.gallery),
icon: const Icon( // ),
Icons.link, // TextButton.icon(
color: Colors.cyanAccent, // icon: const Icon(
), // Icons.link,
label: Text('Link'.i18n), // color: Colors.cyanAccent,
onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), // ),
) // label: Text('Link'.i18n),
], // onPressed: () => Navigator.pop(ctx, MediaPickSetting.link),
), // )
), // ],
); // ),
// ),
/// For image picking logic // );
static Future<void> 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,
);
}
static Future<String?> _pickImage( // /// For image picking logic
ImageSource source, // static Future<void> handleImageButtonTap(
OnImagePickCallback onImagePickCallback, // BuildContext context,
) async { // QuillController controller,
final pickedFile = await ImagePicker().pickImage(source: source); // ImageSource imageSource,
if (pickedFile == null) { // OnImagePickCallback onImagePickCallback, {
return null; // 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<String?> _pickImageDesktop( // controller.insertImageBlock(
BuildContext context, // imageUrl: imageUrl,
FilePickImpl filePickImpl, // );
OnImagePickCallback onImagePickCallback, // }
) async {
final filePath = await filePickImpl(context);
if (filePath == null || filePath.isEmpty) return null;
final file = File(filePath); // static Future<String?> _pickImage(
return onImagePickCallback(file); // ImageSource source,
} // OnImagePickCallback onImagePickCallback,
// ) async {
// final pickedFile = await ImagePicker().pickImage(source: source);
// if (pickedFile == null) {
// return null;
// }
/// For video picking logic // return onImagePickCallback(File(pickedFile.path));
static Future<void> 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);
}
}
static Future<String?> _pickVideo( // static Future<String?> _pickImageDesktop(
ImageSource source, OnVideoPickCallback onVideoPickCallback) async { // BuildContext context,
final pickedFile = await ImagePicker().pickVideo(source: source); // FilePickImpl filePickImpl,
if (pickedFile == null) { // OnImagePickCallback onImagePickCallback,
return null; // ) 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<String?> _pickVideoDesktop( // /// For video picking logic
BuildContext context, // static Future<void> handleVideoButtonTap(
FilePickImpl filePickImpl, // BuildContext context,
OnVideoPickCallback onVideoPickCallback) async { // QuillController controller,
final filePath = await filePickImpl(context); // ImageSource videoSource,
if (filePath == null || filePath.isEmpty) return null; // OnVideoPickCallback onVideoPickCallback, {
// FilePickImpl? filePickImpl,
// WebVideoPickImpl? webVideoPickImpl,
// }) async {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
final file = File(filePath); // String? videoUrl;
return onVideoPickCallback(file); // 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<String?> _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<String?> _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);
// }
// }

@ -137,7 +137,7 @@ class QuillToolbarVideoButton extends StatelessWidget {
Future<void> _typeLink(BuildContext context) async { Future<void> _typeLink(BuildContext context) async {
final value = await showDialog<String>( final value = await showDialog<String>(
context: context, context: context,
builder: (_) => LinkDialog( builder: (_) => TypeLinkDialog(
dialogTheme: options.dialogTheme, dialogTheme: options.dialogTheme,
), ),
); );

@ -1,7 +1,10 @@
import 'dart:io' show File; import 'dart:io' show File;
import 'package:flutter/foundation.dart' show immutable; 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 // 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 // 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; final SaveImageResultMethod method;
} }
Future<SaveImageResult> saveImage(String imageUrl) async { Future<SaveImageResult> saveImage({
final imageSaverService = ImageSaverService.getInstance(); required String imageUrl,
required BuildContext context,
}) async {
final imageSaverService =
QuillSharedExtensionsConfigurations.get(context: context)
.imageSaverService;
final imageFile = File(imageUrl); final imageFile = File(imageUrl);
final hasPermission = await imageSaverService.hasAccess(); final hasPermission = await imageSaverService.hasAccess();
final imageExistsLocally = await imageFile.exists();
if (!hasPermission) { if (!hasPermission) {
await imageSaverService.requestAccess(); await imageSaverService.requestAccess();
} }
final imageExistsLocally = await imageFile.exists();
if (!imageExistsLocally) { if (!imageExistsLocally) {
try { try {
await imageSaverService.saveImageFromNetwork( await imageSaverService.saveImageFromNetwork(

@ -11,11 +11,11 @@ import '../../../embeds/embed_types.dart';
class QuillEditorImageEmbedConfigurations { class QuillEditorImageEmbedConfigurations {
const QuillEditorImageEmbedConfigurations({ const QuillEditorImageEmbedConfigurations({
this.forceUseMobileOptionMenuForImageClick = false, this.forceUseMobileOptionMenuForImageClick = false,
this.onImageRemovedCallback, ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
this.shouldRemoveImageCallback, this.shouldRemoveImageCallback,
this.imageProviderBuilder, this.imageProviderBuilder,
this.imageErrorWidgetBuilder, this.imageErrorWidgetBuilder,
}); }) : _onImageRemovedCallback = onImageRemovedCallback;
/// [onImageRemovedCallback] is called when an image is /// [onImageRemovedCallback] is called when an image is
/// removed from the editor. /// 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 /// [shouldRemoveImageCallback] is a callback
/// function that is invoked when the /// 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 @immutable

@ -2,6 +2,8 @@ import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import '../../../../embeds/embed_types.dart'; import '../../../../embeds/embed_types.dart';
import '../../../../embeds/embed_types/image.dart';
import 'image.dart';
class QuillToolbarCameraButtonExtraOptions class QuillToolbarCameraButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions { extends QuillToolbarBaseButtonExtraOptions {
@ -15,12 +17,9 @@ class QuillToolbarCameraButtonExtraOptions
class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions<
QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> { QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> {
const QuillToolbarCameraButtonOptions({ const QuillToolbarCameraButtonOptions({
required this.onImagePickCallback,
required this.onVideoPickCallback, required this.onVideoPickCallback,
this.webImagePickImpl, this.imageConfigurations = const QuillToolbarImageButtonConfigurations(),
this.webVideoPickImpl, this.webVideoPickImpl,
this.filePickImpl,
this.cameraPickSettingSelector,
this.iconSize, this.iconSize,
this.fillColor, this.fillColor,
super.iconData, super.iconData,
@ -31,18 +30,12 @@ class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions<
super.controller, super.controller,
}); });
final OnImagePickCallback onImagePickCallback; final QuillToolbarImageButtonConfigurations imageConfigurations;
final OnVideoPickCallback onVideoPickCallback; final OnVideoPickCallback onVideoPickCallback;
final WebImagePickImpl? webImagePickImpl;
final WebVideoPickImpl? webVideoPickImpl; final WebVideoPickImpl? webVideoPickImpl;
final FilePickImpl? filePickImpl;
final MediaPickSettingSelector? cameraPickSettingSelector;
final double? iconSize; final double? iconSize;
final Color? fillColor; final Color? fillColor;

@ -2,7 +2,9 @@ import 'package:flutter/widgets.dart' show Color;
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable; import 'package:meta/meta.dart' show immutable;
import '../../../../../logic/extensions/controller.dart';
import '../../../../embeds/embed_types.dart'; import '../../../../embeds/embed_types.dart';
import '../../../../embeds/embed_types/image.dart';
class QuillToolbarImageButtonExtraOptions class QuillToolbarImageButtonExtraOptions
extends QuillToolbarBaseButtonExtraOptions { extends QuillToolbarBaseButtonExtraOptions {
@ -27,27 +29,37 @@ class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions<
super.childBuilder, super.childBuilder,
super.iconTheme, super.iconTheme,
this.fillColor, this.fillColor,
this.onImagePickCallback,
this.filePickImpl,
this.webImagePickImpl,
this.mediaPickSettingSelector,
this.dialogTheme, this.dialogTheme,
this.linkRegExp, this.linkRegExp,
this.imageButtonConfigurations =
const QuillToolbarImageButtonConfigurations(),
}); });
final double? iconSize; final double? iconSize;
final Color? fillColor; 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 OnImagePickedCallback? onImagePickedCallback;
final RegExp? linkRegExp;
final OnImageInsertCallback? _onImageInsertCallback;
OnImageInsertCallback get onImageInsertCallback {
return _onImageInsertCallback ?? defaultOnImageInsertCallback();
}
} }

@ -76,9 +76,7 @@ class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions<
final AutovalidateMode autovalidateMode; final AutovalidateMode autovalidateMode;
final String? validationMessage; final String? validationMessage;
final OnImagePickCallback onImagePickCallback;
final FilePickImpl? filePickImpl; final FilePickImpl? filePickImpl;
final WebImagePickImpl? webImagePickImpl;
final OnVideoPickCallback onVideoPickCallback; final OnVideoPickCallback onVideoPickCallback;
final WebVideoPickImpl? webVideoPickImpl; final WebVideoPickImpl? webVideoPickImpl;
} }

@ -39,8 +39,6 @@ class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions<
final FilePickImpl? filePickImpl; final FilePickImpl? filePickImpl;
final MediaPickSettingSelector? mediaPickSettingSelector;
final Color? fillColor; final Color? fillColor;
final double? iconSize; final double? iconSize;

@ -40,11 +40,12 @@ dependencies:
math_keyboard: ^0.2.1 math_keyboard: ^0.2.1
url_launcher: ^6.2.1 url_launcher: ^6.2.1
meta: ^1.9.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 # In case you are working on changes for both libraries, please uncomment this section
# dependency_overrides: dependency_overrides:
# flutter_quill: flutter_quill:
# path: ../ path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save