pull/1507/head
Ellet 1 year ago committed by X Code
parent 11dd8e7d39
commit eeb720e097
  1. 3
      CHANGELOG.md
  2. 4
      example/ios/Runner/Info.plist
  3. 148
      example/lib/pages/home_page.dart
  4. 7
      example/lib/widgets/demo_scaffold.dart
  5. 26
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  6. 10
      flutter_quill_extensions/lib/logic/extensions/controller.dart
  7. 36
      flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart
  8. 37
      flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart
  9. 66
      flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart
  10. 158
      flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart
  11. 37
      flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart
  12. 31
      flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart
  13. 2
      flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart
  14. 533
      flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button.dart
  15. 533
      flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart
  16. 57
      flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart
  17. 89
      flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart
  18. 13
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart
  19. 25
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart
  20. 2
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart
  21. 9
      flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart
  22. 4
      lib/src/models/config/shared_configurations.dart
  23. 2
      pubspec.yaml

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

@ -21,7 +21,9 @@
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Need to save image</string>
<string>The app will use it to pick images</string>
<key>NSCameraUsageDescription</key>
<string>The app will use it to capture a images, record videos.</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>

@ -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<HomePage> {
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<HomePage> {
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<HomePage> {
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<HomePage> {
return copiedFile.path.toString();
}
Future<String?> _webImagePickImpl(
OnImagePickCallback onImagePickCallback) async {
final result = await FilePicker.platform.pickFiles();
if (result == null) {
return null;
}
// Future<String?> _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<HomePage> {
return copiedFile.path.toString();
}
// ignore: unused_element
Future<MediaPickSetting?> _selectMediaPickSetting(BuildContext context) =>
showDialog<MediaPickSetting>(
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<MediaPickSetting?> _selectCameraPickSetting(BuildContext context) =>
showDialog<MediaPickSetting>(
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<MediaPickSetting?> _selectMediaPickSetting(BuildContext context) =>
// showDialog<MediaPickSetting>(
// 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<MediaPickSetting?> _selectCameraPickSetting(BuildContext context) =>
// showDialog<MediaPickSetting>(
// 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);

@ -89,9 +89,10 @@ class _DemoScaffoldState extends State<DemoScaffold> {
return QuillToolbar(
configurations: QuillToolbarConfigurations(
embedButtons: FlutterQuillEmbeds.toolbarButtons(
imageButtonOptions: QuillToolbarImageButtonOptions(
filePickImpl: openFileSystemPickerForDesktop,
),
// ignore: avoid_redundant_argument_values
imageButtonOptions: const QuillToolbarImageButtonOptions(
// filePickImpl: openFileSystemPickerForDesktop,
),
),
),
);

@ -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(

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

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

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

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

@ -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<CameraAction?> _getCameraAction(BuildContext context) async {
final customCallback =
options.cameraConfigurations.onRequestCameraActionCallback;
if (customCallback != null) {
return await customCallback(context);
}
final cameraAction = await showDialog<CameraAction>(
context: context,
builder: (ctx) => const SelectCameraActionDialog(),
);
return cameraAction;
}
Future<void> _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<MediaPickSetting>(
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;
// }
}
}

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

@ -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;

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

@ -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<void> _onPressedHandler(BuildContext context) async {
if (options.onMediaPickedCallback == null) {
_inputLink(context);
return;
}
final mediaSource = await showDialog<MediaPickSetting>(
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<void> _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<MediaFileUrl?> _pickMediaFileUrl() async {
final mediaFile = await options.mediaFilePicker?.call(options.type);
return mediaFile != null
? options.onMediaPickedCallback?.call(mediaFile)
: null;
}
void _inputLink(BuildContext context) {
showDialog<String>(
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<MediaLinkDialog> createState() => _MediaLinkDialogState();
}
class _MediaLinkDialogState extends State<MediaLinkDialog> {
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: <Widget>[
Icon(icon),
SizedBox(height: gap),
Flexible(child: Text(label)),
],
),
),
),
);
}
}
/// Default file picker.
// Future<QuillFile?> _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;
// }

@ -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<void> _onPressedHandler(BuildContext context) async {
// if (options.onMediaPickedCallback == null) {
// _inputLink(context);
// return;
// }
// final mediaSource = await showDialog<MediaPickSetting>(
// 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<void> _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<MediaFileUrl?> _pickMediaFileUrl() async {
// final mediaFile = await options.mediaFilePicker?.call(options.type);
// return mediaFile != null
// ? options.onMediaPickedCallback?.call(mediaFile)
// : null;
// }
// void _inputLink(BuildContext context) {
// showDialog<String>(
// 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<MediaLinkDialog> createState() => _MediaLinkDialogState();
// }
// class _MediaLinkDialogState extends State<MediaLinkDialog> {
// 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: <Widget>[
// Icon(icon),
// SizedBox(height: gap),
// Flexible(child: Text(label)),
// ],
// ),
// ),
// ),
// );
// }
// }
// /// Default file picker.
// // Future<QuillFile?> _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;
// // }

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

@ -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<void> _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<void> _typeLink(BuildContext context) async {
Future<String?> _typeLink(BuildContext context) async {
final value = await showDialog<String>(
context: context,
builder: (_) => TypeLinkDialog(
dialogTheme: options.dialogTheme,
linkType: LinkType.video,
),
);
_linkSubmitted(value);
return value;
}
void _linkSubmitted(String? value) {

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

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

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

@ -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;

@ -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<String, Object?> extraConfigurations;
@override
List<Object?> get props => [
dialogBarrierColor,

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

Loading…
Cancel
Save