Embed refactor (#933)

* Moved all embed code to seperate folder

* Consolidated embed builders

* Moved embed toolbar items out

* Moved embed code to separate package

* Updated imports

* Removed I from interface names

* Refactored embed button implementation

* Update readme
pull/934/head
Jon Salmon 3 years ago committed by GitHub
parent 4b46df25e7
commit 82d4bf76e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      CHANGELOG.md
  2. 88
      README.md
  3. 105
      example/lib/pages/home_page.dart
  4. 4
      example/lib/pages/read_only_page.dart
  5. 121
      example/lib/universal_ui/universal_ui.dart
  6. 12
      example/lib/widgets/demo_scaffold.dart
  7. 6
      example/pubspec.yaml
  8. 10
      flutter_quill_extensions/.metadata
  9. 3
      flutter_quill_extensions/CHANGELOG.md
  10. 1
      flutter_quill_extensions/LICENSE
  11. 22
      flutter_quill_extensions/README.md
  12. 37
      flutter_quill_extensions/analysis_options.yaml
  13. 282
      flutter_quill_extensions/lib/embeds/builders.dart
  14. 20
      flutter_quill_extensions/lib/embeds/embed_types.dart
  15. 9
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  16. 7
      flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart
  17. 8
      flutter_quill_extensions/lib/embeds/toolbar/image_button.dart
  18. 19
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  19. 8
      flutter_quill_extensions/lib/embeds/toolbar/video_button.dart
  20. 5
      flutter_quill_extensions/lib/embeds/utils.dart
  21. 10
      flutter_quill_extensions/lib/embeds/widgets/image.dart
  22. 2
      flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart
  23. 3
      flutter_quill_extensions/lib/embeds/widgets/video_app.dart
  24. 3
      flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart
  25. 94
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  26. 36
      flutter_quill_extensions/pubspec.yaml
  27. 6
      lib/extensions.dart
  28. 2
      lib/flutter_quill.dart
  29. 10
      lib/src/widgets/delegate.dart
  30. 48
      lib/src/widgets/editor.dart
  31. 24
      lib/src/widgets/embeds.dart
  32. 260
      lib/src/widgets/embeds/default_embed_builder.dart
  33. 5
      lib/src/widgets/raw_editor.dart
  34. 2
      lib/src/widgets/text_block.dart
  35. 2
      lib/src/widgets/text_line.dart
  36. 94
      lib/src/widgets/toolbar.dart
  37. 3
      lib/translations.dart
  38. 7
      pubspec.yaml

@ -1,3 +1,23 @@
# [6.0.0] BREAKING CHANGE
* Removed embed (image, video & forumla) blocks from the package to reduce app size.
These blocks have been moved to the package `flutter_quill_extensions`, migrate by filling the `embedBuilders` and `embedButtons` parameters as follows:
```
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
QuillEditor.basic(
controller: controller,
embedBuilders: FlutterQuillEmbeds.builders,
);
QuillToolbar.basic(
controller: controller,
embedButtons: FlutterQuillEmbeds.buttons(),
);
```
# [5.4.2]
* Upgrade i18n_extension.

@ -153,6 +153,31 @@ QuillToolbar.basic(
]
```
## Embed Blocks
As of version 6.0, embed blocks are not provided by default as part of this package. Instead, this packet provides an interface to all the user to provide there own implementations for embed blocks. Implementations for image, video and forumal embed blocks is proved in a separate package `flutter_quill_extensions`.
Provide a list of embed
### Using the embed blocks from `flutter_quill_extensions`
```
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
QuillEditor.basic(
controller: controller,
embedBuilders: FlutterQuillEmbeds.builders,
);
QuillToolbar.basic(
controller: controller,
embedButtons: FlutterQuillEmbeds.buttons(),
);
```
### Custom Size Image for Mobile
Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follows:
@ -171,7 +196,7 @@ Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follo
Sometimes you want to add some custom content inside your text, custom widgets inside of them. An example is adding notes to the text, or anything custom that you want to add in your text editor.
The only thing that you need is to add a `CustomBlockEmbed` and map it into the `customElementsEmbedBuilder`, to transform the data inside of the Custom Block into a widget!
The only thing that you need is to add a `CustomBlockEmbed` and provider a builder for it to the `embedBuilders` parameter, to transform the data inside of the Custom Block into a widget!
Here is an example:
@ -194,35 +219,40 @@ After that, we need to map this "notes" type into a widget. In that case, I used
Don't forget to add this method to the `QuillEditor` after that!
```dart
Widget customElementsEmbedBuilder(
BuildContext context,
QuillController controller,
CustomBlockEmbed block,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
switch (block.type) {
case 'notes':
final notes = NotesBlockEmbed(block.data).document;
return Material(
color: Colors.transparent,
child: ListTile(
title: Text(
notes.toPlainText().replaceAll('\n', ' '),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.notes),
onTap: () => _addEditNote(context, document: notes),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
class NotesEmbedBuilder implements EmbedBuilder {
NotesEmbedBuilder({required this.addEditNote});
Future<void> Function(BuildContext context, {Document? document}) addEditNote;
@override
String get key => 'notes';
@override
Widget build(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
void Function(GlobalKey<State<StatefulWidget>> videoContainerKey)?
onVideoInit) {
final notes = NotesBlockEmbed(node.value.data).document;
return Material(
color: Colors.transparent,
child: ListTile(
title: Text(
notes.toPlainText().replaceAll('\n', ' '),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.notes),
onTap: () => addEditNote(context, document: notes),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
);
default:
return const SizedBox();
),
);
}
}
```

@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tuple/tuple.dart';
@ -119,7 +120,10 @@ class _HomePageState extends State<HomePage> {
null),
sizeSmall: const TextStyle(fontSize: 9),
),
customElementsEmbedBuilder: customElementsEmbedBuilder,
embedBuilders: [
...FlutterQuillEmbeds.builders,
NotesEmbedBuilder(addEditNote: _addEditNote)
],
);
if (kIsWeb) {
quillEditor = QuillEditor(
@ -145,34 +149,40 @@ class _HomePageState extends State<HomePage> {
null),
sizeSmall: const TextStyle(fontSize: 9),
),
embedBuilder: defaultEmbedBuilderWeb);
embedBuilders: defaultEmbedBuildersWeb);
}
var toolbar = QuillToolbar.basic(
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,
// uncomment to provide a custom "pick from" dialog.
// cameraPickSettingSelector: _selectCameraPickSetting,
embedButtons: FlutterQuillEmbeds.buttons(
// 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,
// uncomment to provide a custom "pick from" dialog.
// cameraPickSettingSelector: _selectCameraPickSetting,
),
showAlignmentButtons: true,
);
if (kIsWeb) {
toolbar = QuillToolbar.basic(
controller: _controller!,
onImagePickCallback: _onImagePickCallback,
webImagePickImpl: _webImagePickImpl,
embedButtons: FlutterQuillEmbeds.buttons(
onImagePickCallback: _onImagePickCallback,
webImagePickImpl: _webImagePickImpl,
),
showAlignmentButtons: true,
);
}
if (_isDesktop()) {
toolbar = QuillToolbar.basic(
controller: _controller!,
onImagePickCallback: _onImagePickCallback,
filePickImpl: openFileSystemPickerForDesktop,
embedButtons: FlutterQuillEmbeds.buttons(
onImagePickCallback: _onImagePickCallback,
filePickImpl: openFileSystemPickerForDesktop,
),
showAlignmentButtons: true,
);
}
@ -386,37 +396,42 @@ class _HomePageState extends State<HomePage> {
controller.replaceText(index, length, block, null);
}
}
}
Widget customElementsEmbedBuilder(
BuildContext context,
QuillController controller,
CustomBlockEmbed block,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
switch (block.type) {
case 'notes':
final notes = NotesBlockEmbed(block.data).document;
return Material(
color: Colors.transparent,
child: ListTile(
title: Text(
notes.toPlainText().replaceAll('\n', ' '),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.notes),
onTap: () => _addEditNote(context, document: notes),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
),
);
default:
return const SizedBox();
}
class NotesEmbedBuilder implements EmbedBuilder {
NotesEmbedBuilder({required this.addEditNote});
Future<void> Function(BuildContext context, {Document? document}) addEditNote;
@override
String get key => 'notes';
@override
Widget build(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
void Function(GlobalKey<State<StatefulWidget>> videoContainerKey)?
onVideoInit) {
final notes = NotesBlockEmbed(node.value.data).document;
return Material(
color: Colors.transparent,
child: ListTile(
title: Text(
notes.toPlainText().replaceAll('\n', ' '),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.notes),
onTap: () => addEditNote(context, document: notes),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
),
);
}
}

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import '../universal_ui/universal_ui.dart';
import '../widgets/demo_scaffold.dart';
@ -38,6 +39,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
readOnly: !_edit,
expands: false,
padding: EdgeInsets.zero,
embedBuilders: FlutterQuillEmbeds.builders,
);
if (kIsWeb) {
quillEditor = QuillEditor(
@ -49,7 +51,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
readOnly: !_edit,
expands: false,
padding: EdgeInsets.zero,
embedBuilder: defaultEmbedBuilderWeb);
embedBuilders: defaultEmbedBuildersWeb);
}
return Padding(
padding: const EdgeInsets.all(8),

@ -3,6 +3,7 @@ library universal_ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:universal_html/html.dart' as html;
import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart';
@ -26,66 +27,72 @@ class UniversalUI {
var ui = UniversalUI();
Widget defaultEmbedBuilderWeb(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
switch (node.value.type) {
case BlockEmbed.imageType:
final imageUrl = node.value.data;
if (isImageBase64(imageUrl)) {
// TODO: handle imageUrl of base64
return const SizedBox();
}
final size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
return Padding(
padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context)
? size.width * 0.5
: (ResponsiveWidget.isLargeScreen(context))
? size.width * 0.75
: size.width * 0.2,
),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.45,
child: HtmlElementView(
viewType: imageUrl,
),
class ImageEmbedBuilderWeb implements EmbedBuilder {
@override
String get key => BlockEmbed.imageType;
@override
Widget build(BuildContext context, QuillController controller, Embed node,
bool readOnly, void Function(GlobalKey videoContainerKey)? onVideoInit) {
final imageUrl = node.value.data;
if (isImageBase64(imageUrl)) {
// TODO: handle imageUrl of base64
return const SizedBox();
}
final size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
return Padding(
padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context)
? size.width * 0.5
: (ResponsiveWidget.isLargeScreen(context))
? size.width * 0.75
: size.width * 0.2,
),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.45,
child: HtmlElementView(
viewType: imageUrl,
),
);
case BlockEmbed.videoType:
var videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
final youtubeID = YoutubePlayer.convertUrlToId(videoUrl);
if (youtubeID != null) {
videoUrl = 'https://www.youtube.com/embed/$youtubeID';
}
),
);
}
}
class VideoEmbedBuilderWeb implements EmbedBuilder {
@override
String get key => BlockEmbed.videoType;
@override
Widget build(BuildContext context, QuillController controller, Embed node,
bool readOnly, void Function(GlobalKey videoContainerKey)? onVideoInit) {
var videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
final youtubeID = YoutubePlayer.convertUrlToId(videoUrl);
if (youtubeID != null) {
videoUrl = 'https://www.youtube.com/embed/$youtubeID';
}
}
UniversalUI().platformViewRegistry.registerViewFactory(
videoUrl,
(id) => html.IFrameElement()
..width = MediaQuery.of(context).size.width.toString()
..height = MediaQuery.of(context).size.height.toString()
..src = videoUrl
..style.border = 'none');
UniversalUI().platformViewRegistry.registerViewFactory(
videoUrl,
(id) => html.IFrameElement()
..width = MediaQuery.of(context).size.width.toString()
..height = MediaQuery.of(context).size.height.toString()
..src = videoUrl
..style.border = 'none');
return SizedBox(
height: 500,
child: HtmlElementView(
viewType: videoUrl,
),
);
default:
throw UnimplementedError(
'Embeddable type "${node.value.type}" is not supported by default '
'embed builder of QuillEditor. You must pass your own builder function '
'to embedBuilder property of QuillEditor or QuillField widgets.',
);
return SizedBox(
height: 500,
child: HtmlElementView(
viewType: videoUrl,
),
);
}
}
List<EmbedBuilder> get defaultEmbedBuildersWeb => [
ImageEmbedBuilderWeb(),
VideoEmbedBuilderWeb(),
];

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:path_provider/path_provider.dart';
typedef DemoContentBuilder = Widget Function(
@ -89,11 +90,16 @@ class _DemoScaffoldState extends State<DemoScaffold> {
return const Scaffold(body: Center(child: Text('Loading...')));
}
final actions = widget.actions ?? <Widget>[];
var toolbar = QuillToolbar.basic(controller: _controller!);
var toolbar = QuillToolbar.basic(
controller: _controller!,
embedButtons: FlutterQuillEmbeds.buttons(),
);
if (_isDesktop()) {
toolbar = QuillToolbar.basic(
controller: _controller!,
filePickImpl: openFileSystemPickerForDesktop);
controller: _controller!,
embedButtons: FlutterQuillEmbeds.buttons(
filePickImpl: openFileSystemPickerForDesktop),
);
}
return Scaffold(
key: _scaffoldKey,

@ -34,6 +34,12 @@ dependencies:
file_picker: ^4.6.1
flutter_quill:
path: ../
flutter_quill_extensions:
path: ../flutter_quill_extensions
dependency_overrides:
flutter_quill:
path: ../
dev_dependencies:
flutter_test:

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
channel: stable
project_type: package

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

@ -0,0 +1 @@
TODO: Add your license here.

@ -0,0 +1,22 @@
# Flutter Quill Extensions
Helpers to support embed widgets in flutter_quill.
## Usage
Set the `embedBuilders` and `embedToolbar` params in `QuillEditor` and `QuillToolbar` with the
values provided by this repository.
```
QuillEditor.basic(
controller: controller,
embedBuilders: FlutterQuillEmbeds.builders,
);
```
```
QuillToolbar.basic(
controller: controller,
embedButtons: FlutterQuillEmbeds.buttons(),
);
```

@ -0,0 +1,37 @@
include: package:pedantic/analysis_options.yaml
analyzer:
errors:
undefined_prefixed_name: ignore
unsafe_html: ignore
linter:
rules:
- always_declare_return_types
- always_put_required_named_parameters_first
- annotate_overrides
- avoid_empty_else
- avoid_escaping_inner_quotes
- avoid_print
- avoid_redundant_argument_values
- avoid_types_on_closure_parameters
- avoid_void_async
- cascade_invocations
- directives_ordering
- lines_longer_than_80_chars
- omit_local_variable_types
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_initializing_formals
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_relative_imports
- prefer_single_quotes
- sort_constructors_first
- sort_unnamed_constructors_first
- unnecessary_lambdas
- unnecessary_parenthesis
- unnecessary_string_interpolations

@ -0,0 +1,282 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill/translations.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:math_keyboard/math_keyboard.dart';
import 'package:tuple/tuple.dart';
import 'utils.dart';
import 'widgets/image.dart';
import 'widgets/image_resizer.dart';
import 'widgets/video_app.dart';
import 'widgets/youtube_video_app.dart';
class ImageEmbedBuilder implements EmbedBuilder {
@override
String get key => BlockEmbed.imageType;
@override
Widget build(
BuildContext context,
QuillController controller,
base.Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
var image;
final imageUrl = standardizeImageUrl(node.value.data);
Tuple2<double?, double?>? _widthHeight;
final style = node.style.attributes['style'];
if (base.isMobile() && style != null) {
final _attrs = base.parseKeyValuePairs(style.value.toString(), {
Attribute.mobileWidth,
Attribute.mobileHeight,
Attribute.mobileMargin,
Attribute.mobileAlignment
});
if (_attrs.isNotEmpty) {
assert(
_attrs[Attribute.mobileWidth] != null &&
_attrs[Attribute.mobileHeight] != null,
'mobileWidth and mobileHeight must be specified');
final w = double.parse(_attrs[Attribute.mobileWidth]!);
final h = double.parse(_attrs[Attribute.mobileHeight]!);
_widthHeight = Tuple2(w, h);
final m = _attrs[Attribute.mobileMargin] == null
? 0.0
: double.parse(_attrs[Attribute.mobileMargin]!);
final a = base.getAlignment(_attrs[Attribute.mobileAlignment]);
image = Padding(
padding: EdgeInsets.all(m),
child: imageByUrl(imageUrl, width: w, height: h, alignment: a));
}
}
if (_widthHeight == null) {
image = imageByUrl(imageUrl);
_widthHeight = Tuple2((image as Image).width, image.height);
}
if (!readOnly && base.isMobile()) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final resizeOption = _SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final _screenSize = MediaQuery.of(context).size;
return ImageResizer(
onImageResize: (w, h) {
final res = getEmbedNode(
controller, controller.selection.start);
final attr = base.replaceStyleString(
getImageStyleString(controller), w, h);
controller
..skipRequestKeyboard = true
..formatText(
res.item1, 1, StyleAttribute(attr));
},
imageWidth: _widthHeight?.item1,
imageHeight: _widthHeight?.item2,
maxWidth: _screenSize.width,
maxHeight: _screenSize.height);
});
},
);
final copyOption = _SimpleDialogItem(
icon: Icons.copy_all_outlined,
color: Colors.cyanAccent,
text: 'Copy'.i18n,
onPressed: () {
final imageNode =
getEmbedNode(controller, controller.selection.start)
.item2;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl =
Tuple2(imageUrl, getImageStyleString(controller));
Navigator.pop(context);
},
);
final removeOption = _SimpleDialogItem(
icon: Icons.delete_forever_outlined,
color: Colors.red.shade200,
text: 'Remove'.i18n,
onPressed: () {
final offset =
getEmbedNode(controller, controller.selection.start)
.item1;
controller.replaceText(offset, 1, '',
TextSelection.collapsed(offset: offset));
Navigator.pop(context);
},
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(10))),
children: [resizeOption, copyOption, removeOption]),
);
});
},
child: image);
}
if (!readOnly || !base.isMobile() || isImageBase64(imageUrl)) {
return image;
}
// We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage(context, imageUrl, image);
}
}
class VideoEmbedBuilder implements EmbedBuilder {
@override
String get key => BlockEmbed.videoType;
@override
Widget build(
BuildContext context,
QuillController controller,
base.Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit) {
assert(!kIsWeb, 'Please provide video EmbedBuilder for Web');
final videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
return YoutubeVideoApp(
videoUrl: videoUrl, context: context, readOnly: readOnly);
}
return VideoApp(
videoUrl: videoUrl,
context: context,
readOnly: readOnly,
onVideoInit: onVideoInit,
);
}
}
class FormulaEmbedBuilder implements EmbedBuilder {
@override
String get key => BlockEmbed.formulaType;
@override
Widget build(
BuildContext context,
QuillController controller,
base.Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit) {
assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web');
final mathController = MathFieldEditingController();
return Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
// If the MathField is tapped, hides the built in keyboard
SystemChannels.textInput.invokeMethod('TextInput.hide');
debugPrint(mathController.currentEditingValue());
}
},
child: MathField(
controller: mathController,
variables: const ['x', 'y', 'z'],
onChanged: (value) {},
onSubmitted: (value) {},
),
);
}
}
Widget _menuOptionsForReadonlyImage(
BuildContext context, String imageUrl, Widget image) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final saveOption = _SimpleDialogItem(
icon: Icons.save,
color: Colors.greenAccent,
text: 'Save'.i18n,
onPressed: () {
imageUrl = appendFileExtensionToImageUrl(imageUrl);
GallerySaver.saveImage(imageUrl).then((_) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Saved'.i18n)));
Navigator.pop(context);
});
},
);
final zoomOption = _SimpleDialogItem(
icon: Icons.zoom_in,
color: Colors.cyanAccent,
text: 'Zoom'.i18n,
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
ImageTapWrapper(imageUrl: imageUrl)));
},
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
children: [saveOption, zoomOption]),
);
});
},
child: image);
}
class _SimpleDialogItem extends StatelessWidget {
const _SimpleDialogItem(
{required this.icon,
required this.color,
required this.text,
required this.onPressed,
Key? key})
: super(key: key);
final IconData icon;
final Color color;
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SimpleDialogOption(
onPressed: onPressed,
child: Row(
children: [
Icon(icon, size: 36, color: color),
Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child:
Text(text, style: const TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
}

@ -0,0 +1,20 @@
import 'dart:io';
import 'package:flutter/material.dart';
typedef OnImagePickCallback = Future<String?> Function(File file);
typedef OnVideoPickCallback = Future<String?> Function(File file);
typedef FilePickImpl = Future<String?> Function(BuildContext context);
typedef WebImagePickImpl = Future<String?> Function(
OnImagePickCallback onImagePickCallback);
typedef WebVideoPickImpl = Future<String?> Function(
OnVideoPickCallback onImagePickCallback);
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(
BuildContext context);
enum MediaPickSetting {
Gallery,
Link,
Camera,
Video,
}

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:image_picker/image_picker.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../../translations/toolbar.i18n.dart';
import '../controller.dart';
import '../toolbar.dart';
import 'package:flutter_quill/translations.dart';
import '../embed_types.dart';
import 'image_video_utils.dart';
class CameraButton extends StatelessWidget {
const CameraButton({

@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/themes/quill_dialog_theme.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
import '../embed_types.dart';
class FormulaButton extends StatelessWidget {
const FormulaButton({

@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/themes/quill_dialog_theme.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
import '../embed_types.dart';
import 'image_video_utils.dart';
class ImageButton extends StatelessWidget {
const ImageButton({

@ -2,15 +2,13 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
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';
import 'package:flutter_quill/translations.dart';
import 'package:flutter_quill/extensions.dart';
import '../embed_types.dart';
class LinkDialog extends StatefulWidget {
const LinkDialog({this.dialogTheme, this.link, Key? key}) : super(key: key);
@ -75,13 +73,6 @@ class LinkDialogState extends State<LinkDialog> {
}
}
enum MediaPickSetting {
Gallery,
Link,
Camera,
Video,
}
class ImageVideoUtils {
static Future<MediaPickSetting?> selectMediaPickSetting(
BuildContext context,

@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/themes/quill_dialog_theme.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
import '../embed_types.dart';
import 'image_video_utils.dart';
class VideoButton extends StatelessWidget {
const VideoButton({

@ -0,0 +1,5 @@
import 'package:string_validator/string_validator.dart';
bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl);
}

@ -2,12 +2,10 @@ import 'dart:convert';
import 'dart:io' as io;
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:photo_view/photo_view.dart';
import 'package:string_validator/string_validator.dart';
import '../../models/documents/attribute.dart';
import '../../models/documents/style.dart';
import '../controller.dart';
import '../utils.dart';
const List<String> imageFileExtensions = [
'.jpeg',
@ -19,10 +17,6 @@ const List<String> imageFileExtensions = [
'.heic'
];
bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl);
}
String getImageStyleString(QuillController controller) {
final String? s = controller
.getAllSelectionStyles()

@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../../translations/toolbar.i18n.dart';
import 'package:flutter_quill/translations.dart';
class ImageResizer extends StatefulWidget {
const ImageResizer(

@ -2,11 +2,10 @@ import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart';
import '../../../flutter_quill.dart';
/// Widget for playing back video
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
class VideoApp extends StatefulWidget {

@ -1,10 +1,9 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart';
import '../default_styles.dart';
class YoutubeVideoApp extends StatefulWidget {
const YoutubeVideoApp(
{required this.videoUrl, required this.context, required this.readOnly});

@ -0,0 +1,94 @@
library flutter_quill_extensions;
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'embeds/builders.dart';
import 'embeds/embed_types.dart';
import 'embeds/toolbar/camera_button.dart';
import 'embeds/toolbar/formula_button.dart';
import 'embeds/toolbar/image_button.dart';
import 'embeds/toolbar/video_button.dart';
export 'embeds/embed_types.dart';
export 'embeds/toolbar/camera_button.dart';
export 'embeds/toolbar/formula_button.dart';
export 'embeds/toolbar/image_button.dart';
export 'embeds/toolbar/image_video_utils.dart';
export 'embeds/toolbar/video_button.dart';
export 'embeds/utils.dart';
class FlutterQuillEmbeds {
static List<EmbedBuilder> get builders => [
ImageEmbedBuilder(),
VideoEmbedBuilder(),
FormulaEmbedBuilder(),
];
static List<EmbedButtonBuilder> buttons({
bool showImageButton = true,
bool showVideoButton = true,
bool showCameraButton = true,
bool showFormulaButton = false,
OnImagePickCallback? onImagePickCallback,
OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector,
MediaPickSettingSelector? cameraPickSettingSelector,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl,
}) {
return [
if (showImageButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton(
icon: Icons.image,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
),
if (showVideoButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton(
icon: Icons.movie_creation,
iconSize: toolbarIconSize,
controller: controller,
onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webVideoPickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
),
if ((onImagePickCallback != null || onVideoPickCallback != null) &&
showCameraButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton(
icon: Icons.photo_camera,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
webVideoPickImpl: webVideoPickImpl,
cameraPickSettingSelector: cameraPickSettingSelector,
iconTheme: iconTheme,
),
if (showFormulaButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => FormulaButton(
icon: Icons.functions,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
)
];
}
}

@ -0,0 +1,36 @@
name: flutter_quill_extensions
description: Embed extensions for flutter_quill
version: 0.0.1
homepage: https://bulletjournal.us/home/index.html
#author: bulletjournal
repository: https://github.com/singerdmx/flutter-quill/flutter_quill_extensions
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_quill: ^6.0.0
image_picker: ^0.8.5+3
photo_view: ^0.14.0
video_player: ^2.4.2
youtube_player_flutter_quill: ^8.2.2
gallery_saver: ^2.3.2
math_keyboard: ^0.1.6
string_validator: ^0.3.0
dependency_overrides:
flutter_quill:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
pedantic: ^1.11.1
# The following section is specific to Flutter packages.
flutter:

@ -0,0 +1,6 @@
library flutter_quill.extensions;
export 'src/models/documents/nodes/leaf.dart' hide Text;
export 'src/models/rules/insert.dart';
export 'src/utils/platform.dart';
export 'src/utils/string.dart';

@ -14,7 +14,7 @@ export 'src/utils/embeds.dart';
export 'src/widgets/controller.dart';
export 'src/widgets/default_styles.dart';
export 'src/widgets/editor.dart';
export 'src/widgets/embeds/image.dart';
export 'src/widgets/embeds.dart';
export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction;
export 'src/widgets/style_widgets/style_widgets.dart';
export 'src/widgets/toolbar.dart';

@ -6,7 +6,7 @@ import 'package:flutter/scheduler.dart';
import '../../flutter_quill.dart';
import 'text_selection.dart';
typedef EmbedBuilder = Widget Function(
typedef EmbedsBuilder = Widget Function(
BuildContext context,
QuillController controller,
Embed node,
@ -14,14 +14,6 @@ typedef EmbedBuilder = Widget Function(
void Function(GlobalKey videoContainerKey)? onVideoInit,
);
typedef CustomEmbedBuilder = Widget Function(
BuildContext context,
QuillController controller,
CustomBlockEmbed block,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
);
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);
/// Delegate interface for the [EditorTextSelectionGestureDetectorBuilder].

@ -12,6 +12,7 @@ import 'package:tuple/tuple.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/documents/style.dart';
import '../utils/platform.dart';
import 'box.dart';
@ -19,7 +20,7 @@ import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'embeds/default_embed_builder.dart';
import 'embeds.dart';
import 'float_cursor.dart';
import 'link.dart';
import 'raw_editor.dart';
@ -168,8 +169,7 @@ class QuillEditor extends StatefulWidget {
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.embedBuilder = defaultEmbedBuilder,
this.customElementsEmbedBuilder,
this.embedBuilders,
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
this.customStyleBuilder,
this.locale,
@ -182,6 +182,7 @@ class QuillEditor extends StatefulWidget {
required QuillController controller,
required bool readOnly,
Brightness? keyboardAppearance,
Iterable<EmbedBuilder>? embedBuilders,
/// The locale to use for the editor toolbar, defaults to system locale
/// More at https://github.com/singerdmx/flutter-quill#translation
@ -198,6 +199,7 @@ class QuillEditor extends StatefulWidget {
padding: EdgeInsets.zero,
keyboardAppearance: keyboardAppearance ?? Brightness.light,
locale: locale,
embedBuilders: embedBuilders,
);
}
@ -346,8 +348,7 @@ class QuillEditor extends StatefulWidget {
LongPressEndDetails details, TextPosition Function(Offset offset))?
onSingleLongTapEnd;
final EmbedBuilder embedBuilder;
final CustomEmbedBuilder? customElementsEmbedBuilder;
final Iterable<EmbedBuilder>? embedBuilders;
final CustomStyleBuilder? customStyleBuilder;
/// The locale to use for the editor toolbar, defaults to system locale
@ -473,23 +474,28 @@ class QuillEditorState extends State<QuillEditor>
readOnly,
onVideoInit,
) {
final customElementsEmbedBuilder = widget.customElementsEmbedBuilder;
final isCustomType = node.value.type == BlockEmbed.customType;
if (customElementsEmbedBuilder != null && isCustomType) {
return customElementsEmbedBuilder(
context,
controller,
CustomBlockEmbed.fromJsonString(node.value.data),
readOnly,
onVideoInit,
);
final builders = widget.embedBuilders;
if (builders != null) {
var _node = node;
// Creates correct node for custom embed
if (node.value.type == BlockEmbed.customType) {
_node = Embed(CustomBlockEmbed.fromJsonString(node.value.data));
}
for (final builder in builders) {
if (builder.key == _node.value.type) {
return builder.build(
context, controller, _node, readOnly, onVideoInit);
}
}
}
return widget.embedBuilder(
context,
controller,
node,
readOnly,
onVideoInit,
throw UnimplementedError(
'Embeddable type "${node.value.type}" is not supported by supplied '
'embed builders. You must pass your own builder function to '
'embedBuilders property of QuillEditor or QuillField widgets.',
);
},
linkActionPickerDelegate: widget.linkActionPickerDelegate,

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/themes/quill_dialog_theme.dart';
import '../models/themes/quill_icon_theme.dart';
import 'controller.dart';
abstract class EmbedBuilder {
String get key;
Widget build(
BuildContext context,
QuillController controller,
leaf.Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
);
}
typedef EmbedButtonBuilder = Widget Function(
QuillController controller,
double toolbarIconSize,
QuillIconTheme? iconTheme,
QuillDialogTheme? dialogTheme);

@ -1,260 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:math_keyboard/math_keyboard.dart';
import 'package:tuple/tuple.dart';
import '../../models/documents/attribute.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart' as leaf;
import '../../translations/toolbar.i18n.dart';
import '../../utils/embeds.dart';
import '../../utils/platform.dart';
import '../../utils/string.dart';
import '../controller.dart';
import 'image.dart';
import 'image_resizer.dart';
import 'video_app.dart';
import 'youtube_video_app.dart';
Widget defaultEmbedBuilder(
BuildContext context,
QuillController controller,
leaf.Embed node,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
Tuple2<double?, double?>? _widthHeight;
switch (node.value.type) {
case BlockEmbed.imageType:
final imageUrl = standardizeImageUrl(node.value.data);
var image;
final style = node.style.attributes['style'];
if (isMobile() && style != null) {
final _attrs = parseKeyValuePairs(style.value.toString(), {
Attribute.mobileWidth,
Attribute.mobileHeight,
Attribute.mobileMargin,
Attribute.mobileAlignment
});
if (_attrs.isNotEmpty) {
assert(
_attrs[Attribute.mobileWidth] != null &&
_attrs[Attribute.mobileHeight] != null,
'mobileWidth and mobileHeight must be specified');
final w = double.parse(_attrs[Attribute.mobileWidth]!);
final h = double.parse(_attrs[Attribute.mobileHeight]!);
_widthHeight = Tuple2(w, h);
final m = _attrs[Attribute.mobileMargin] == null
? 0.0
: double.parse(_attrs[Attribute.mobileMargin]!);
final a = getAlignment(_attrs[Attribute.mobileAlignment]);
image = Padding(
padding: EdgeInsets.all(m),
child: imageByUrl(imageUrl, width: w, height: h, alignment: a));
}
}
if (_widthHeight == null) {
image = imageByUrl(imageUrl);
_widthHeight = Tuple2((image as Image).width, image.height);
}
if (!readOnly && isMobile()) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final resizeOption = _SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final _screenSize = MediaQuery.of(context).size;
return ImageResizer(
onImageResize: (w, h) {
final res = getEmbedNode(
controller, controller.selection.start);
final attr = replaceStyleString(
getImageStyleString(controller), w, h);
controller
..skipRequestKeyboard = true
..formatText(
res.item1, 1, StyleAttribute(attr));
},
imageWidth: _widthHeight?.item1,
imageHeight: _widthHeight?.item2,
maxWidth: _screenSize.width,
maxHeight: _screenSize.height);
});
},
);
final copyOption = _SimpleDialogItem(
icon: Icons.copy_all_outlined,
color: Colors.cyanAccent,
text: 'Copy'.i18n,
onPressed: () {
final imageNode =
getEmbedNode(controller, controller.selection.start)
.item2;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl =
Tuple2(imageUrl, getImageStyleString(controller));
Navigator.pop(context);
},
);
final removeOption = _SimpleDialogItem(
icon: Icons.delete_forever_outlined,
color: Colors.red.shade200,
text: 'Remove'.i18n,
onPressed: () {
final offset =
getEmbedNode(controller, controller.selection.start)
.item1;
controller.replaceText(offset, 1, '',
TextSelection.collapsed(offset: offset));
Navigator.pop(context);
},
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(10))),
children: [resizeOption, copyOption, removeOption]),
);
});
},
child: image);
}
if (!readOnly || !isMobile() || isImageBase64(imageUrl)) {
return image;
}
// We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage(context, imageUrl, image);
case BlockEmbed.videoType:
final videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
return YoutubeVideoApp(
videoUrl: videoUrl, context: context, readOnly: readOnly);
}
return VideoApp(
videoUrl: videoUrl,
context: context,
readOnly: readOnly,
onVideoInit: onVideoInit,
);
case BlockEmbed.formulaType:
final mathController = MathFieldEditingController();
return Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
// If the MathField is tapped, hides the built in keyboard
SystemChannels.textInput.invokeMethod('TextInput.hide');
debugPrint(mathController.currentEditingValue());
}
},
child: MathField(
controller: mathController,
variables: const ['x', 'y', 'z'],
onChanged: (value) {},
onSubmitted: (value) {},
),
);
default:
throw UnimplementedError(
'Embeddable type "${node.value.type}" is not supported by default '
'embed builder of QuillEditor. You must pass your own builder function '
'to embedBuilder property of QuillEditor or QuillField widgets.',
);
}
}
Widget _menuOptionsForReadonlyImage(
BuildContext context, String imageUrl, Widget image) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final saveOption = _SimpleDialogItem(
icon: Icons.save,
color: Colors.greenAccent,
text: 'Save'.i18n,
onPressed: () {
imageUrl = appendFileExtensionToImageUrl(imageUrl);
GallerySaver.saveImage(imageUrl).then((_) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Saved'.i18n)));
Navigator.pop(context);
});
},
);
final zoomOption = _SimpleDialogItem(
icon: Icons.zoom_in,
color: Colors.cyanAccent,
text: 'Zoom'.i18n,
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
ImageTapWrapper(imageUrl: imageUrl)));
},
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
children: [saveOption, zoomOption]),
);
});
},
child: image);
}
class _SimpleDialogItem extends StatelessWidget {
const _SimpleDialogItem(
{required this.icon,
required this.color,
required this.text,
required this.onPressed,
Key? key})
: super(key: key);
final IconData icon;
final Color color;
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SimpleDialogOption(
onPressed: onPressed,
child: Row(
children: [
Icon(icon, size: 36, color: color),
Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child:
Text(text, style: const TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
}

@ -26,7 +26,6 @@ import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'editor.dart';
import 'embeds/default_embed_builder.dart';
import 'keyboard_listener.dart';
import 'link.dart';
import 'proxy.dart';
@ -46,6 +45,7 @@ class RawEditor extends StatefulWidget {
required this.cursorStyle,
required this.selectionColor,
required this.selectionCtrls,
required this.embedBuilder,
Key? key,
this.scrollable = true,
this.padding = EdgeInsets.zero,
@ -70,7 +70,6 @@ class RawEditor extends StatefulWidget {
this.keyboardAppearance = Brightness.light,
this.enableInteractiveSelection = true,
this.scrollPhysics,
this.embedBuilder = defaultEmbedBuilder,
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
this.customStyleBuilder,
this.floatingCursorDisabled = false})
@ -221,7 +220,7 @@ class RawEditor extends StatefulWidget {
final ScrollPhysics? scrollPhysics;
/// Builder function for embeddable objects.
final EmbedBuilder embedBuilder;
final EmbedsBuilder embedBuilder;
final LinkActionPickerDelegate linkActionPickerDelegate;
final CustomStyleBuilder? customStyleBuilder;
final bool floatingCursorDisabled;

@ -79,7 +79,7 @@ class EditableTextBlock extends StatelessWidget {
final bool enableInteractiveSelection;
final bool hasFocus;
final EdgeInsets? contentPadding;
final EmbedBuilder embedBuilder;
final EmbedsBuilder embedBuilder;
final LinkActionPicker linkActionPicker;
final ValueChanged<String>? onLaunchUrl;
final CustomStyleBuilder? customStyleBuilder;

@ -45,7 +45,7 @@ class TextLine extends StatefulWidget {
final Line line;
final TextDirection? textDirection;
final EmbedBuilder embedBuilder;
final EmbedsBuilder embedBuilder;
final DefaultStyles styles;
final bool readOnly;
final QuillController controller;

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:i18n_extension/i18n_widget.dart';
@ -10,14 +8,11 @@ import '../models/themes/quill_icon_theme.dart';
import '../translations/toolbar.i18n.dart';
import '../utils/font.dart';
import 'controller.dart';
import 'embeds.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/formula_button.dart';
import 'toolbar/history_button.dart';
import 'toolbar/image_button.dart';
import 'toolbar/image_video_utils.dart';
import 'toolbar/indent_button.dart';
import 'toolbar/link_style_button.dart';
import 'toolbar/quill_font_family_button.dart';
@ -28,13 +23,10 @@ import 'toolbar/select_alignment_button.dart';
import 'toolbar/select_header_style_button.dart';
import 'toolbar/toggle_check_list_button.dart';
import 'toolbar/toggle_style_button.dart';
import 'toolbar/video_button.dart';
export 'toolbar/clear_format_button.dart';
export 'toolbar/color_button.dart';
export 'toolbar/history_button.dart';
export 'toolbar/image_button.dart';
export 'toolbar/image_video_utils.dart';
export 'toolbar/indent_button.dart';
export 'toolbar/link_style_button.dart';
export 'toolbar/quill_font_size_button.dart';
@ -43,17 +35,6 @@ export 'toolbar/select_alignment_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);
typedef FilePickImpl = Future<String?> Function(BuildContext context);
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;
@ -69,7 +50,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
this.toolbarSectionSpacing = 4,
this.multiRowsDisplay = true,
this.color,
this.filePickImpl,
this.customButtons = const [],
this.locale,
Key? key,
@ -108,19 +88,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
bool showUndo = true,
bool showRedo = true,
bool multiRowsDisplay = true,
bool showImageButton = true,
bool showVideoButton = true,
bool showFormulaButton = false,
bool showCameraButton = true,
bool showDirection = false,
bool showSearchButton = true,
OnImagePickCallback? onImagePickCallback,
OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector,
MediaPickSettingSelector? cameraPickSettingSelector,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl,
List<QuillCustomButton> customButtons = const [],
///Map of font sizes in string
@ -129,6 +98,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
///Map of font families in string
Map<String, String>? fontFamilyValues,
/// Toolbar items to display for controls of embed blocks
List<EmbedButtonBuilder>? embedButtons,
///The theme to use for the icons in the toolbar, uses type [QuillIconTheme]
QuillIconTheme? iconTheme,
@ -153,8 +125,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
showColorButton ||
showBackgroundColorButton ||
showClearFormat ||
onImagePickCallback != null ||
onVideoPickCallback != null,
embedButtons?.isNotEmpty == true,
showAlignmentButtons || showDirection,
showLeftAlignment,
showCenterAlignment,
@ -330,56 +301,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
controller: controller,
iconTheme: iconTheme,
),
if (showImageButton)
ImageButton(
icon: Icons.image,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
),
if (showVideoButton)
VideoButton(
icon: Icons.movie_creation,
iconSize: toolbarIconSize,
controller: controller,
onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webVideoPickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
),
if ((onImagePickCallback != null || onVideoPickCallback != null) &&
showCameraButton)
CameraButton(
icon: Icons.photo_camera,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
onVideoPickCallback: onVideoPickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
webVideoPickImpl: webVideoPickImpl,
cameraPickSettingSelector: cameraPickSettingSelector,
iconTheme: iconTheme,
),
if (showFormulaButton)
FormulaButton(
icon: Icons.functions,
iconSize: toolbarIconSize,
controller: controller,
onImagePickCallback: onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
mediaPickSettingSelector: mediaPickSettingSelector,
iconTheme: iconTheme,
dialogTheme: dialogTheme,
),
if (embedButtons != null)
for (final builder in embedButtons)
builder(controller, toolbarIconSize, iconTheme, dialogTheme),
if (showDividers &&
isButtonGroupShown[0] &&
(isButtonGroupShown[1] ||
@ -554,8 +478,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
/// is given.
final Color? color;
final FilePickImpl? filePickImpl;
/// The locale to use for the editor toolbar, defaults to system locale
/// More https://github.com/singerdmx/flutter-quill#translation
final Locale? locale;

@ -0,0 +1,3 @@
library flutter_quill.translations;
export 'src/translations/toolbar.i18n.dart';

@ -15,22 +15,15 @@ dependencies:
collection: ^1.16.0
flutter_colorpicker: ^1.0.3
flutter_keyboard_visibility: ^5.2.0
image_picker: ^0.8.5+3
photo_view: ^0.14.0
quiver: ^3.1.0
string_validator: ^0.3.0
tuple: ^2.0.0
url_launcher: ^6.1.2
pedantic: ^1.11.1
video_player: ^2.4.2
characters: ^1.2.0
youtube_player_flutter_quill: ^8.2.2
diff_match_patch: ^0.4.1
i18n_extension: ^5.0.1
gallery_saver: ^2.3.2
device_info_plus: ^4.0.0
platform: ^3.1.0
math_keyboard: ^0.1.6
dev_dependencies:
flutter_test:

Loading…
Cancel
Save