From db00a9ba6cff201051d34f762e75506ac52703ce Mon Sep 17 00:00:00 2001 From: Junho Lee Date: Sun, 7 Aug 2022 21:08:49 +0900 Subject: [PATCH] Add custom toolbar for photo and video. --- example/lib/pages/home_page.dart | 30 +++- lib/src/widgets/toolbar.dart | 8 +- lib/src/widgets/toolbar/camera_button.dart | 72 ++++---- .../widgets/toolbar/camera_video_utils.dart | 155 ++++++++++++++++++ 4 files changed, 219 insertions(+), 46 deletions(-) create mode 100644 lib/src/widgets/toolbar/camera_video_utils.dart diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index d8019f43..9e1c63c3 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -156,6 +156,8 @@ class _HomePageState extends State { onVideoPickCallback: _onVideoPickCallback, // uncomment to provide a custom "pick from" dialog. // mediaPickSettingSelector: _selectMediaPickSetting, + // uncomment to provide a custom "pick from" dialog. + // cameraPickSettingSelector: _selectCameraPickSetting, showAlignmentButtons: true, ); if (kIsWeb) { @@ -271,6 +273,32 @@ class _HomePageState extends State { ), ); + Future _selectCameraPickSetting(BuildContext context) => + showDialog( + context: context, + builder: (ctx) => + AlertDialog( + contentPadding: EdgeInsets.zero, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton.icon( + icon: const Icon(Icons.camera), + label: const Text('Capture a photo'), + onPressed: () => + Navigator.pop(ctx, CameraPickSetting.Camera), + ), + TextButton.icon( + icon: const Icon(Icons.video_call), + label: const Text('Capture a video'), + onPressed: () => + Navigator.pop(ctx, CameraPickSetting.Video), + ) + ], + ), + ), + ); + Widget _buildMenuBar(BuildContext context) { final size = MediaQuery.of(context).size; const itemStyle = TextStyle( @@ -400,4 +428,4 @@ class NotesBlockEmbed extends CustomBlockEmbed { NotesBlockEmbed(jsonEncode(document.toDelta().toJson())); Document get document => Document.fromJson(jsonDecode(data)); -} +} \ No newline at end of file diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 40cffdf0..e1317b48 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -18,6 +18,7 @@ import 'toolbar/formula_button.dart'; import 'toolbar/history_button.dart'; import 'toolbar/image_button.dart'; import 'toolbar/image_video_utils.dart'; +import 'toolbar/camera_video_utils.dart'; import 'toolbar/indent_button.dart'; import 'toolbar/link_style_button.dart'; import 'toolbar/quill_font_family_button.dart'; @@ -35,6 +36,7 @@ export 'toolbar/color_button.dart'; export 'toolbar/history_button.dart'; export 'toolbar/image_button.dart'; export 'toolbar/image_video_utils.dart'; +export 'toolbar/camera_video_utils.dart'; export 'toolbar/indent_button.dart'; export 'toolbar/link_style_button.dart'; export 'toolbar/quill_font_size_button.dart'; @@ -54,6 +56,8 @@ typedef WebVideoPickImpl = Future Function( OnVideoPickCallback onImagePickCallback); typedef MediaPickSettingSelector = Future Function( BuildContext context); +typedef CameraPickSettingSelector = Future Function( + BuildContext context); // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -117,6 +121,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { OnImagePickCallback? onImagePickCallback, OnVideoPickCallback? onVideoPickCallback, MediaPickSettingSelector? mediaPickSettingSelector, + CameraPickSettingSelector? cameraPickSettingSelector, FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, WebVideoPickImpl? webVideoPickImpl, @@ -364,6 +369,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl, webVideoPickImpl: webVideoPickImpl, + cameraPickSettingSelector: cameraPickSettingSelector, iconTheme: iconTheme, ), if (showFormulaButton) @@ -583,4 +589,4 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/src/widgets/toolbar/camera_button.dart b/lib/src/widgets/toolbar/camera_button.dart index 184200c9..0948a03c 100644 --- a/lib/src/widgets/toolbar/camera_button.dart +++ b/lib/src/widgets/toolbar/camera_button.dart @@ -16,6 +16,7 @@ class CameraButton extends StatelessWidget { this.filePickImpl, this.webImagePickImpl, this.webVideoPickImpl, + this.cameraPickSettingSelector, this.iconTheme, Key? key, }) : super(key: key); @@ -37,6 +38,8 @@ class CameraButton extends StatelessWidget { final FilePickImpl? filePickImpl; + final CameraPickSettingSelector? cameraPickSettingSelector; + final QuillIconTheme? iconTheme; @override @@ -54,7 +57,7 @@ class CameraButton extends StatelessWidget { size: iconSize * 1.77, fillColor: iconFillColor, borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _handleCameraButtonTap(context, controller, + onPressed: () => _onPressedHandler(context, controller, onImagePickCallback: onImagePickCallback, onVideoPickCallback: onVideoPickCallback, filePickImpl: filePickImpl, @@ -62,55 +65,36 @@ class CameraButton extends StatelessWidget { ); } - Future _handleCameraButtonTap( + Future _onPressedHandler( BuildContext context, QuillController controller, {OnImagePickCallback? onImagePickCallback, OnVideoPickCallback? onVideoPickCallback, FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl}) async { if (onImagePickCallback != null && onVideoPickCallback != null) { - // Show dialog to choose Photo or Video - return await showDialog( - context: context, - builder: (context) { - return AlertDialog( - contentPadding: const EdgeInsets.all(0), - backgroundColor: Colors.transparent, - content: Column(mainAxisSize: MainAxisSize.min, children: [ - TextButton.icon( - icon: const Icon(Icons.photo, color: Colors.cyanAccent), - label: const Text('Photo'), - onPressed: () { - ImageVideoUtils.handleImageButtonTap(context, controller, - ImageSource.camera, onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl); - }, - ), - TextButton.icon( - icon: const Icon(Icons.movie_creation, - color: Colors.orangeAccent), - label: const Text('Video'), - onPressed: () { - ImageVideoUtils.handleVideoButtonTap(context, controller, - ImageSource.camera, onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl); - }, - ) - ])); - }); - } - if (onImagePickCallback != null) { - return ImageVideoUtils.handleImageButtonTap( - context, controller, ImageSource.camera, onImagePickCallback, - filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl); + final selector = + cameraPickSettingSelector ?? CameraVideoUtils.selectMediaPickSetting; + + final source = await selector(context); + if (source != null) { + switch (source) { + case CameraPickSetting.Camera: + CameraVideoUtils.handleCameraButtonTap(context, controller, + ImageSource.camera, onImagePickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl); + break; + case CameraPickSetting.Video: + CameraVideoUtils.handleVideoButtonTap(context, controller, + ImageSource.camera, onVideoPickCallback, + filePickImpl: filePickImpl, + webVideoPickImpl: webVideoPickImpl); + break; + default: + throw ArgumentError('Invalid MediaSetting'); + } + } } - - assert(onVideoPickCallback != null, 'onVideoPickCallback must not be null'); - return ImageVideoUtils.handleVideoButtonTap( - context, controller, ImageSource.camera, onVideoPickCallback!, - filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl); } -} +} \ No newline at end of file diff --git a/lib/src/widgets/toolbar/camera_video_utils.dart b/lib/src/widgets/toolbar/camera_video_utils.dart new file mode 100644 index 00000000..37ab74dd --- /dev/null +++ b/lib/src/widgets/toolbar/camera_video_utils.dart @@ -0,0 +1,155 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../models/documents/nodes/embeddable.dart'; +import '../../models/rules/insert.dart'; +import '../../models/themes/quill_dialog_theme.dart'; +import '../../translations/toolbar.i18n.dart'; +import '../../utils/platform.dart'; +import '../controller.dart'; +import '../toolbar.dart'; + +enum CameraPickSetting { + Camera, + Video, +} + +class CameraVideoUtils { + static Future selectMediaPickSetting( + BuildContext context, + ) => + showDialog( + context: context, + builder: (ctx) => 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('Camera'), + onPressed: () => Navigator.pop(ctx, CameraPickSetting.Camera), + ), + TextButton.icon( + icon: const Icon( + Icons.video_call, + color: Colors.cyanAccent, + ), + label: Text('Video'), + onPressed: () => Navigator.pop(ctx, CameraPickSetting.Video), + ) + ], + ), + ), + ); + + static Future handleCameraButtonTap( + BuildContext context, + QuillController controller, + ImageSource imageSource, + OnImagePickCallback onImagePickCallback, + {FilePickImpl? filePickImpl, + WebImagePickImpl? webImagePickImpl}) async { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + + String? imageUrl; + if (kIsWeb) { + assert( + webImagePickImpl != null, + 'Please provide webImagePickImpl for Web ' + '(check out example directory for how to do it)'); + imageUrl = await webImagePickImpl!(onImagePickCallback); + } 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) { + controller.replaceText(index, length, BlockEmbed.image(imageUrl), null); + } + } + + static Future _pickImage( + ImageSource source, OnImagePickCallback onImagePickCallback) async { + final pickedFile = await ImagePicker().pickImage(source: source); + if (pickedFile == null) { + return null; + } + + return onImagePickCallback(File(pickedFile.path)); + } + + static Future _pickImageDesktop( + BuildContext context, + FilePickImpl filePickImpl, + OnImagePickCallback onImagePickCallback) async { + final filePath = await filePickImpl(context); + if (filePath == null || filePath.isEmpty) return null; + + final file = File(filePath); + return onImagePickCallback(file); + } + + /// For video picking logic + static Future handleVideoButtonTap( + BuildContext context, + QuillController controller, + ImageSource videoSource, + OnVideoPickCallback onVideoPickCallback, + {FilePickImpl? filePickImpl, + WebVideoPickImpl? webVideoPickImpl}) async { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + + String? videoUrl; + if (kIsWeb) { + assert( + webVideoPickImpl != null, + 'Please provide webVideoPickImpl for Web ' + '(check out example directory for how to do it)'); + videoUrl = await webVideoPickImpl!(onVideoPickCallback); + } else if (isMobile()) { + videoUrl = await _pickVideo(videoSource, onVideoPickCallback); + } else { + assert(filePickImpl != null, 'Desktop must provide filePickImpl'); + videoUrl = + await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback); + } + + if (videoUrl != null) { + controller.replaceText(index, length, BlockEmbed.video(videoUrl), null); + } + } + + static Future _pickVideo( + ImageSource source, OnVideoPickCallback onVideoPickCallback) async { + final pickedFile = await ImagePicker().pickVideo(source: source); + if (pickedFile == null) { + return null; + } + + return onVideoPickCallback(File(pickedFile.path)); + } + + static Future _pickVideoDesktop( + BuildContext context, + FilePickImpl filePickImpl, + OnVideoPickCallback onVideoPickCallback) async { + final filePath = await filePickImpl(context); + if (filePath == null || filePath.isEmpty) return null; + + final file = File(filePath); + return onVideoPickCallback(file); + } +} \ No newline at end of file