From 853aea3820f60c72a334a18e5ad1a423b2cbe893 Mon Sep 17 00:00:00 2001
From: Xin Yao <singerdmx@gmail.com>
Date: Tue, 20 Jul 2021 11:48:06 -0700
Subject: [PATCH] Refactor out camera button in toolbar

---
 lib/src/widgets/toolbar.dart                  |  26 ++--
 lib/src/widgets/toolbar/camera_button.dart    |  80 ++++++++++++
 lib/src/widgets/toolbar/image_button.dart     |  61 +---------
 .../widgets/toolbar/image_video_utils.dart    | 115 ++++++++++++++++++
 lib/src/widgets/toolbar/video_button.dart     |  61 +---------
 5 files changed, 220 insertions(+), 123 deletions(-)
 create mode 100644 lib/src/widgets/toolbar/camera_button.dart
 create mode 100644 lib/src/widgets/toolbar/image_video_utils.dart

diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart
index f31819eb..1071754c 100644
--- a/lib/src/widgets/toolbar.dart
+++ b/lib/src/widgets/toolbar.dart
@@ -2,11 +2,11 @@ import 'dart:io';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:image_picker/image_picker.dart';
 
 import '../models/documents/attribute.dart';
 import 'controller.dart';
 import 'toolbar/arrow_indicated_button_list.dart';
+import 'toolbar/camera_button.dart';
 import 'toolbar/clear_format_button.dart';
 import 'toolbar/color_button.dart';
 import 'toolbar/history_button.dart';
@@ -82,6 +82,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
     OnVideoPickCallback? onVideoPickCallback,
     FilePickImpl? filePickImpl,
     WebImagePickImpl? webImagePickImpl,
+    WebVideoPickImpl? webVideoPickImpl,
     Key? key,
   }) {
     final isButtonGroupShown = [
@@ -173,7 +174,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
             icon: Icons.image,
             iconSize: toolbarIconSize,
             controller: controller,
-            imageSource: ImageSource.gallery,
             onImagePickCallback: onImagePickCallback,
             filePickImpl: filePickImpl,
             webImagePickImpl: webImagePickImpl,
@@ -183,21 +183,21 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
             icon: Icons.movie_creation,
             iconSize: toolbarIconSize,
             controller: controller,
-            videoSource: ImageSource.gallery,
             onVideoPickCallback: onVideoPickCallback,
             filePickImpl: filePickImpl,
             webVideoPickImpl: webImagePickImpl,
           ),
-        if (onImagePickCallback != null && showCamera)
-          ImageButton(
-            icon: Icons.photo_camera,
-            iconSize: toolbarIconSize,
-            controller: controller,
-            imageSource: ImageSource.camera,
-            onImagePickCallback: onImagePickCallback,
-            filePickImpl: filePickImpl,
-            webImagePickImpl: webImagePickImpl,
-          ),
+        if ((onImagePickCallback != null || onVideoPickCallback != null) &&
+            showCamera)
+          CameraButton(
+              icon: Icons.photo_camera,
+              iconSize: toolbarIconSize,
+              controller: controller,
+              onImagePickCallback: onImagePickCallback,
+              onVideoPickCallback: onVideoPickCallback,
+              filePickImpl: filePickImpl,
+              webImagePickImpl: webImagePickImpl,
+              webVideoPickImpl: webVideoPickImpl),
         if (isButtonGroupShown[0] &&
             (isButtonGroupShown[1] ||
                 isButtonGroupShown[2] ||
diff --git a/lib/src/widgets/toolbar/camera_button.dart b/lib/src/widgets/toolbar/camera_button.dart
new file mode 100644
index 00000000..099700f3
--- /dev/null
+++ b/lib/src/widgets/toolbar/camera_button.dart
@@ -0,0 +1,80 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+
+import '../controller.dart';
+import '../toolbar.dart';
+import 'image_video_utils.dart';
+import 'quill_icon_button.dart';
+
+class CameraButton extends StatelessWidget {
+  const CameraButton({
+    required this.icon,
+    required this.controller,
+    this.iconSize = kDefaultIconSize,
+    this.fillColor,
+    this.onImagePickCallback,
+    this.onVideoPickCallback,
+    this.filePickImpl,
+    this.webImagePickImpl,
+    this.webVideoPickImpl,
+    Key? key,
+  }) : super(key: key);
+
+  final IconData icon;
+  final double iconSize;
+
+  final Color? fillColor;
+
+  final QuillController controller;
+
+  final OnImagePickCallback? onImagePickCallback;
+
+  final OnVideoPickCallback? onVideoPickCallback;
+
+  final WebImagePickImpl? webImagePickImpl;
+
+  final WebVideoPickImpl? webVideoPickImpl;
+
+  final FilePickImpl? filePickImpl;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+
+    return QuillIconButton(
+      icon: Icon(icon, size: iconSize, color: theme.iconTheme.color),
+      highlightElevation: 0,
+      hoverElevation: 0,
+      size: iconSize * 1.77,
+      fillColor: fillColor ?? theme.canvasColor,
+      onPressed: () => _handleCameraButtonTap(context, controller,
+          onImagePickCallback: onImagePickCallback,
+          onVideoPickCallback: onVideoPickCallback,
+          filePickImpl: filePickImpl,
+          webImagePickImpl: webImagePickImpl),
+    );
+  }
+
+  Future<void> _handleCameraButtonTap(
+      BuildContext context, QuillController controller,
+      {OnImagePickCallback? onImagePickCallback,
+      OnVideoPickCallback? onVideoPickCallback,
+      FilePickImpl? filePickImpl,
+      WebImagePickImpl? webImagePickImpl}) async {
+    if (onImagePickCallback != null && onVideoPickCallback != null) {
+      // TODO: show dialog to choose Image or Video
+    }
+
+    if (onImagePickCallback != null) {
+      return ImageVideoUtils.handleImageButtonTap(
+          context, controller, ImageSource.camera, onImagePickCallback,
+          filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl);
+    }
+
+    assert(onVideoPickCallback != null, 'onVideoPickCallback must not be null');
+    return ImageVideoUtils.handleVideoButtonTap(
+        context, controller, ImageSource.camera, onVideoPickCallback!,
+        filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl);
+  }
+}
diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart
index 029ad83e..f5aaac51 100644
--- a/lib/src/widgets/toolbar/image_button.dart
+++ b/lib/src/widgets/toolbar/image_button.dart
@@ -1,22 +1,19 @@
-import 'dart:io';
-
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:image_picker/image_picker.dart';
 
-import '../../models/documents/nodes/embed.dart';
 import '../controller.dart';
 import '../toolbar.dart';
+import 'image_video_utils.dart';
 import 'quill_icon_button.dart';
 
 class ImageButton extends StatelessWidget {
   const ImageButton({
     required this.icon,
     required this.controller,
-    required this.imageSource,
+    required this.onImagePickCallback,
     this.iconSize = kDefaultIconSize,
     this.fillColor,
-    this.onImagePickCallback,
     this.filePickImpl,
     this.webImagePickImpl,
     Key? key,
@@ -29,12 +26,10 @@ class ImageButton extends StatelessWidget {
 
   final QuillController controller;
 
-  final OnImagePickCallback? onImagePickCallback;
+  final OnImagePickCallback onImagePickCallback;
 
   final WebImagePickImpl? webImagePickImpl;
 
-  final ImageSource imageSource;
-
   final FilePickImpl? filePickImpl;
 
   @override
@@ -47,53 +42,9 @@ class ImageButton extends StatelessWidget {
       hoverElevation: 0,
       size: iconSize * 1.77,
       fillColor: fillColor ?? theme.canvasColor,
-      onPressed: () => _handleImageButtonTap(context, filePickImpl),
+      onPressed: () => ImageVideoUtils.handleImageButtonTap(
+          context, controller, ImageSource.gallery, onImagePickCallback,
+          filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl),
     );
   }
-
-  Future<void> _handleImageButtonTap(BuildContext context,
-      [FilePickImpl? filePickImpl]) 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 (Platform.isAndroid || Platform.isIOS) {
-      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);
-    }
-  }
-
-  Future<String?> _pickImage(
-      ImageSource source, OnImagePickCallback onImagePickCallback) async {
-    final pickedFile = await ImagePicker().pickImage(source: source);
-    if (pickedFile == null) {
-      return null;
-    }
-
-    return onImagePickCallback(File(pickedFile.path));
-  }
-
-  Future<String?> _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);
-  }
 }
diff --git a/lib/src/widgets/toolbar/image_video_utils.dart b/lib/src/widgets/toolbar/image_video_utils.dart
new file mode 100644
index 00000000..6f044d0d
--- /dev/null
+++ b/lib/src/widgets/toolbar/image_video_utils.dart
@@ -0,0 +1,115 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+
+import '../../models/documents/nodes/embed.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+
+class ImageVideoUtils {
+  /// For image picking logic
+  static Future<void> handleImageButtonTap(
+      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 (Platform.isAndroid || Platform.isIOS) {
+      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<String?> _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<String?> _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<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 '
+          '(check out example directory for how to do it)');
+      videoUrl = await webVideoPickImpl!(onVideoPickCallback);
+    } else if (Platform.isAndroid || Platform.isIOS) {
+      videoUrl = await _pickVideo(ImageSource.gallery, 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);
+  }
+}
diff --git a/lib/src/widgets/toolbar/video_button.dart b/lib/src/widgets/toolbar/video_button.dart
index 54f48a9c..4657dcdc 100644
--- a/lib/src/widgets/toolbar/video_button.dart
+++ b/lib/src/widgets/toolbar/video_button.dart
@@ -1,22 +1,19 @@
-import 'dart:io';
-
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:image_picker/image_picker.dart';
 
-import '../../models/documents/nodes/embed.dart';
 import '../controller.dart';
 import '../toolbar.dart';
+import 'image_video_utils.dart';
 import 'quill_icon_button.dart';
 
 class VideoButton extends StatelessWidget {
   const VideoButton({
     required this.icon,
     required this.controller,
-    required this.videoSource,
+    required this.onVideoPickCallback,
     this.iconSize = kDefaultIconSize,
     this.fillColor,
-    this.onVideoPickCallback,
     this.filePickImpl,
     this.webVideoPickImpl,
     Key? key,
@@ -29,12 +26,10 @@ class VideoButton extends StatelessWidget {
 
   final QuillController controller;
 
-  final OnVideoPickCallback? onVideoPickCallback;
+  final OnVideoPickCallback onVideoPickCallback;
 
   final WebVideoPickImpl? webVideoPickImpl;
 
-  final ImageSource videoSource;
-
   final FilePickImpl? filePickImpl;
 
   @override
@@ -47,53 +42,9 @@ class VideoButton extends StatelessWidget {
       hoverElevation: 0,
       size: iconSize * 1.77,
       fillColor: fillColor ?? theme.canvasColor,
-      onPressed: () => _handleVideoButtonTap(context, filePickImpl),
+      onPressed: () => ImageVideoUtils.handleVideoButtonTap(
+          context, controller, ImageSource.gallery, onVideoPickCallback,
+          filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl),
     );
   }
-
-  Future<void> _handleVideoButtonTap(BuildContext context,
-      [FilePickImpl? filePickImpl]) 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 (Platform.isAndroid || Platform.isIOS) {
-      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);
-    }
-  }
-
-  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));
-  }
-
-  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);
-  }
 }