Skip to content

Commit

Permalink
Add custom toolbar for photo and video. (singerdmx#906)
Browse files Browse the repository at this point in the history
  • Loading branch information
buddypia committed Aug 7, 2022
1 parent 91eb56a commit f0ef795
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 46 deletions.
30 changes: 29 additions & 1 deletion example/lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class _HomePageState extends State<HomePage> {
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) {
Expand Down Expand Up @@ -271,6 +273,32 @@ class _HomePageState extends State<HomePage> {
),
);

Future<CameraPickSetting?> _selectCameraPickSetting(BuildContext context) =>
showDialog<CameraPickSetting>(
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(
Expand Down Expand Up @@ -400,4 +428,4 @@ class NotesBlockEmbed extends CustomBlockEmbed {
NotesBlockEmbed(jsonEncode(document.toDelta().toJson()));

Document get document => Document.fromJson(jsonDecode(data));
}
}
8 changes: 7 additions & 1 deletion lib/src/widgets/toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -54,6 +56,8 @@ typedef WebVideoPickImpl = Future<String?> Function(
OnVideoPickCallback onImagePickCallback);
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(
BuildContext context);
typedef CameraPickSettingSelector = Future<CameraPickSetting?> Function(
BuildContext context);

// The default size of the icon of a button.
const double kDefaultIconSize = 18;
Expand Down Expand Up @@ -117,6 +121,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
OnImagePickCallback? onImagePickCallback,
OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector,
CameraPickSettingSelector? cameraPickSettingSelector,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl,
Expand Down Expand Up @@ -364,6 +369,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
webVideoPickImpl: webVideoPickImpl,
cameraPickSettingSelector: cameraPickSettingSelector,
iconTheme: iconTheme,
),
if (showFormulaButton)
Expand Down Expand Up @@ -583,4 +589,4 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
),
);
}
}
}
72 changes: 28 additions & 44 deletions lib/src/widgets/toolbar/camera_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class CameraButton extends StatelessWidget {
this.filePickImpl,
this.webImagePickImpl,
this.webVideoPickImpl,
this.cameraPickSettingSelector,
this.iconTheme,
Key? key,
}) : super(key: key);
Expand All @@ -37,6 +38,8 @@ class CameraButton extends StatelessWidget {

final FilePickImpl? filePickImpl;

final CameraPickSettingSelector? cameraPickSettingSelector;

final QuillIconTheme? iconTheme;

@override
Expand All @@ -54,63 +57,44 @@ 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,
webImagePickImpl: webImagePickImpl),
);
}

Future<void> _handleCameraButtonTap(
Future<void> _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);
}
}
}
155 changes: 155 additions & 0 deletions lib/src/widgets/toolbar/camera_video_utils.dart
Original file line number Diff line number Diff line change
@@ -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<CameraPickSetting?> selectMediaPickSetting(
BuildContext context,
) =>
showDialog<CameraPickSetting>(
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<void> 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<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 (isMobile()) {
videoUrl = await _pickVideo(videoSource, onVideoPickCallback);
} else {
assert(filePickImpl != null, 'Desktop must provide filePickImpl');
videoUrl =
await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback);
}

if (videoUrl != null) {
controller.replaceText(index, length, BlockEmbed.video(videoUrl), null);
}
}

static Future<String?> _pickVideo(
ImageSource source, OnVideoPickCallback onVideoPickCallback) async {
final pickedFile = await ImagePicker().pickVideo(source: source);
if (pickedFile == null) {
return null;
}

return onVideoPickCallback(File(pickedFile.path));
}

static Future<String?> _pickVideoDesktop(
BuildContext context,
FilePickImpl filePickImpl,
OnVideoPickCallback onVideoPickCallback) async {
final filePath = await filePickImpl(context);
if (filePath == null || filePath.isEmpty) return null;

final file = File(filePath);
return onVideoPickCallback(file);
}
}

0 comments on commit f0ef795

Please sign in to comment.