Add Link as source for images and videos (#316)

* Add Link as source for images and videos

Hosted images and videos are supported, but there's no way to actually insert them.

* Always provide an option to add media from link
pull/324/head
rho-cassiopeiae 4 years ago committed by GitHub
parent 350296e4bf
commit 30df844d17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      example/lib/pages/home_page.dart
  2. 4
      lib/src/utils/media_pick_setting.dart
  3. 39
      lib/src/widgets/link_dialog.dart
  4. 19
      lib/src/widgets/toolbar.dart
  5. 57
      lib/src/widgets/toolbar/image_button.dart
  6. 33
      lib/src/widgets/toolbar/image_video_utils.dart
  7. 41
      lib/src/widgets/toolbar/link_style_button.dart
  8. 57
      lib/src/widgets/toolbar/video_button.dart

@ -141,9 +141,15 @@ class _HomePageState extends State<HomePage> {
embedBuilder: defaultEmbedBuilderWeb);
}
var toolbar = QuillToolbar.basic(
controller: _controller!,
onImagePickCallback: _onImagePickCallback,
onVideoPickCallback: _onVideoPickCallback);
controller: _controller!,
// 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,
onVideoPickCallback: _onVideoPickCallback,
// uncomment to provide a custom "pick from" dialog.
// mediaPickSettingSelector: _selectMediaPickSetting,
);
if (kIsWeb) {
toolbar = QuillToolbar.basic(
controller: _controller!,
@ -228,6 +234,29 @@ class _HomePageState extends State<HomePage> {
return copiedFile.path.toString();
}
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),
)
],
),
),
);
Widget _buildMenuBar(BuildContext context) {
final size = MediaQuery.of(context).size;
const itemStyle = TextStyle(

@ -0,0 +1,4 @@
enum MediaPickSetting {
Gallery,
Link,
}

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class LinkDialog extends StatefulWidget {
const LinkDialog({Key? key}) : super(key: key);
@override
LinkDialogState createState() => LinkDialogState();
}
class LinkDialogState extends State<LinkDialog> {
String _link = '';
@override
Widget build(BuildContext context) {
return AlertDialog(
content: TextField(
decoration: const InputDecoration(labelText: 'Paste a link'),
autofocus: true,
onChanged: _linkChanged,
),
actions: [
TextButton(
onPressed: _link.isNotEmpty ? _applyLink : null,
child: const Text('Ok'),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link);
}
}

@ -1,9 +1,9 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../models/documents/attribute.dart';
import '../utils/media_pick_setting.dart';
import 'controller.dart';
import 'toolbar/arrow_indicated_button_list.dart';
import 'toolbar/camera_button.dart';
@ -19,6 +19,7 @@ import 'toolbar/toggle_check_list_button.dart';
import 'toolbar/toggle_style_button.dart';
import 'toolbar/video_button.dart';
export '../utils/media_pick_setting.dart';
export 'toolbar/clear_format_button.dart';
export 'toolbar/color_button.dart';
export 'toolbar/history_button.dart';
@ -31,6 +32,7 @@ export 'toolbar/quill_icon_button.dart';
export 'toolbar/select_header_style_button.dart';
export 'toolbar/toggle_check_list_button.dart';
export 'toolbar/toggle_style_button.dart';
export 'toolbar/video_button.dart';
typedef OnImagePickCallback = Future<String?> Function(File file);
typedef OnVideoPickCallback = Future<String?> Function(File file);
@ -39,6 +41,8 @@ typedef WebImagePickImpl = Future<String?> Function(
OnImagePickCallback onImagePickCallback);
typedef WebVideoPickImpl = Future<String?> Function(
OnVideoPickCallback onImagePickCallback);
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(
BuildContext context);
// The default size of the icon of a button.
const double kDefaultIconSize = 18;
@ -77,9 +81,12 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
bool showHistory = true,
bool showHorizontalRule = false,
bool multiRowsDisplay = true,
bool showCamera = true,
bool showImageButton = true,
bool showVideoButton = true,
bool showCameraButton = true,
OnImagePickCallback? onImagePickCallback,
OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl,
@ -169,7 +176,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
iconSize: toolbarIconSize,
controller: controller,
),
if (onImagePickCallback != null)
if (showImageButton)
ImageButton(
icon: Icons.image,
iconSize: toolbarIconSize,
@ -177,8 +184,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
onImagePickCallback: onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
),
if (onVideoPickCallback != null)
if (showVideoButton)
VideoButton(
icon: Icons.movie_creation,
iconSize: toolbarIconSize,
@ -186,9 +194,10 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webVideoPickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
),
if ((onImagePickCallback != null || onVideoPickCallback != null) &&
showCamera)
showCameraButton)
CameraButton(
icon: Icons.photo_camera,
iconSize: toolbarIconSize,

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embed.dart';
import '../../utils/media_pick_setting.dart';
import '../controller.dart';
import '../link_dialog.dart';
import '../toolbar.dart';
import 'image_video_utils.dart';
import 'quill_icon_button.dart';
@ -11,11 +13,12 @@ class ImageButton extends StatelessWidget {
const ImageButton({
required this.icon,
required this.controller,
required this.onImagePickCallback,
this.iconSize = kDefaultIconSize,
this.onImagePickCallback,
this.fillColor,
this.filePickImpl,
this.webImagePickImpl,
this.mediaPickSettingSelector,
Key? key,
}) : super(key: key);
@ -26,12 +29,14 @@ class ImageButton extends StatelessWidget {
final QuillController controller;
final OnImagePickCallback onImagePickCallback;
final OnImagePickCallback? onImagePickCallback;
final WebImagePickImpl? webImagePickImpl;
final FilePickImpl? filePickImpl;
final MediaPickSettingSelector? mediaPickSettingSelector;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -42,9 +47,49 @@ class ImageButton extends StatelessWidget {
hoverElevation: 0,
size: iconSize * 1.77,
fillColor: fillColor ?? theme.canvasColor,
onPressed: () => ImageVideoUtils.handleImageButtonTap(
context, controller, ImageSource.gallery, onImagePickCallback,
filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl),
onPressed: () => _onPressedHandler(context),
);
}
Future<void> _onPressedHandler(BuildContext context) async {
if (onImagePickCallback != null) {
final selector =
mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
final source = await selector(context);
if (source != null) {
if (source == MediaPickSetting.Gallery) {
_pickImage(context);
} else {
_typeLink(context);
}
}
} else {
_typeLink(context);
}
}
void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
context,
controller,
ImageSource.gallery,
onImagePickCallback!,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
);
void _typeLink(BuildContext context) {
showDialog<String>(
context: context,
builder: (_) => const LinkDialog(),
).then(_linkSubmitted);
}
void _linkSubmitted(String? value) {
if (value != null && value.isNotEmpty) {
final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index;
controller.replaceText(index, length, BlockEmbed.image(value), null);
}
}
}

@ -5,10 +5,43 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embed.dart';
import '../../utils/media_pick_setting.dart';
import '../controller.dart';
import '../toolbar.dart';
class ImageVideoUtils {
static Future<MediaPickSetting?> selectMediaPickSetting(
BuildContext context,
) =>
showDialog<MediaPickSetting>(
context: context,
builder: (ctx) => AlertDialog(
contentPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton.icon(
icon: const Icon(
Icons.collections,
color: Colors.orangeAccent,
),
label: const Text('Gallery'),
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery),
),
TextButton.icon(
icon: const Icon(
Icons.link,
color: Colors.cyanAccent,
),
label: const Text('Link'),
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link),
)
],
),
),
);
/// For image picking logic
static Future<void> handleImageButtonTap(
BuildContext context,

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../../models/documents/attribute.dart';
import '../controller.dart';
import '../link_dialog.dart';
import '../toolbar.dart';
import 'quill_icon_button.dart';
@ -70,7 +71,7 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
showDialog<String>(
context: context,
builder: (ctx) {
return const _LinkDialog();
return const LinkDialog();
},
).then(_linkSubmitted);
}
@ -82,41 +83,3 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
widget.controller.formatSelection(LinkAttribute(value));
}
}
class _LinkDialog extends StatefulWidget {
const _LinkDialog({Key? key}) : super(key: key);
@override
_LinkDialogState createState() => _LinkDialogState();
}
class _LinkDialogState extends State<_LinkDialog> {
String _link = '';
@override
Widget build(BuildContext context) {
return AlertDialog(
content: TextField(
decoration: const InputDecoration(labelText: 'Paste a link'),
autofocus: true,
onChanged: _linkChanged,
),
actions: [
TextButton(
onPressed: _link.isNotEmpty ? _applyLink : null,
child: const Text('Apply'),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link);
}
}

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embed.dart';
import '../../utils/media_pick_setting.dart';
import '../controller.dart';
import '../link_dialog.dart';
import '../toolbar.dart';
import 'image_video_utils.dart';
import 'quill_icon_button.dart';
@ -11,11 +13,12 @@ class VideoButton extends StatelessWidget {
const VideoButton({
required this.icon,
required this.controller,
required this.onVideoPickCallback,
this.iconSize = kDefaultIconSize,
this.onVideoPickCallback,
this.fillColor,
this.filePickImpl,
this.webVideoPickImpl,
this.mediaPickSettingSelector,
Key? key,
}) : super(key: key);
@ -26,12 +29,14 @@ class VideoButton extends StatelessWidget {
final QuillController controller;
final OnVideoPickCallback onVideoPickCallback;
final OnVideoPickCallback? onVideoPickCallback;
final WebVideoPickImpl? webVideoPickImpl;
final FilePickImpl? filePickImpl;
final MediaPickSettingSelector? mediaPickSettingSelector;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -42,9 +47,49 @@ class VideoButton extends StatelessWidget {
hoverElevation: 0,
size: iconSize * 1.77,
fillColor: fillColor ?? theme.canvasColor,
onPressed: () => ImageVideoUtils.handleVideoButtonTap(
context, controller, ImageSource.gallery, onVideoPickCallback,
filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl),
onPressed: () => _onPressedHandler(context),
);
}
Future<void> _onPressedHandler(BuildContext context) async {
if (onVideoPickCallback != null) {
final selector =
mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
final source = await selector(context);
if (source != null) {
if (source == MediaPickSetting.Gallery) {
_pickVideo(context);
} else {
_typeLink(context);
}
}
} else {
_typeLink(context);
}
}
void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap(
context,
controller,
ImageSource.gallery,
onVideoPickCallback!,
filePickImpl: filePickImpl,
webVideoPickImpl: webVideoPickImpl,
);
void _typeLink(BuildContext context) {
showDialog<String>(
context: context,
builder: (_) => const LinkDialog(),
).then(_linkSubmitted);
}
void _linkSubmitted(String? value) {
if (value != null && value.isNotEmpty) {
final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index;
controller.replaceText(index, length, BlockEmbed.video(value), null);
}
}
}

Loading…
Cancel
Save