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] # [5.4.2]
* Upgrade i18n_extension. * 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 ### Custom Size Image for Mobile
Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follows: 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. 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: 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! Don't forget to add this method to the `QuillEditor` after that!
```dart ```dart
Widget customElementsEmbedBuilder( class NotesEmbedBuilder implements EmbedBuilder {
BuildContext context, NotesEmbedBuilder({required this.addEditNote});
QuillController controller,
CustomBlockEmbed block, Future<void> Function(BuildContext context, {Document? document}) addEditNote;
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit, @override
) { String get key => 'notes';
switch (block.type) {
case 'notes': @override
final notes = NotesBlockEmbed(block.data).document; Widget build(
BuildContext context,
return Material( QuillController controller,
color: Colors.transparent, Embed node,
child: ListTile( bool readOnly,
title: Text( void Function(GlobalKey<State<StatefulWidget>> videoContainerKey)?
notes.toPlainText().replaceAll('\n', ' '), onVideoInit) {
maxLines: 3, final notes = NotesBlockEmbed(node.value.data).document;
overflow: TextOverflow.ellipsis,
), return Material(
leading: const Icon(Icons.notes), color: Colors.transparent,
onTap: () => _addEditNote(context, document: notes), child: ListTile(
shape: RoundedRectangleBorder( title: Text(
borderRadius: BorderRadius.circular(10), notes.toPlainText().replaceAll('\n', ' '),
side: const BorderSide(color: Colors.grey), 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text; 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/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -119,7 +120,10 @@ class _HomePageState extends State<HomePage> {
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: const TextStyle(fontSize: 9),
), ),
customElementsEmbedBuilder: customElementsEmbedBuilder, embedBuilders: [
...FlutterQuillEmbeds.builders,
NotesEmbedBuilder(addEditNote: _addEditNote)
],
); );
if (kIsWeb) { if (kIsWeb) {
quillEditor = QuillEditor( quillEditor = QuillEditor(
@ -145,34 +149,40 @@ class _HomePageState extends State<HomePage> {
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: const TextStyle(fontSize: 9),
), ),
embedBuilder: defaultEmbedBuilderWeb); embedBuilders: defaultEmbedBuildersWeb);
} }
var toolbar = QuillToolbar.basic( var toolbar = QuillToolbar.basic(
controller: _controller!, controller: _controller!,
// provide a callback to enable picking images from device. embedButtons: FlutterQuillEmbeds.buttons(
// if omit, "image" button only allows adding images from url. // provide a callback to enable picking images from device.
// same goes for videos. // if omit, "image" button only allows adding images from url.
onImagePickCallback: _onImagePickCallback, // same goes for videos.
onVideoPickCallback: _onVideoPickCallback, onImagePickCallback: _onImagePickCallback,
// uncomment to provide a custom "pick from" dialog. onVideoPickCallback: _onVideoPickCallback,
// mediaPickSettingSelector: _selectMediaPickSetting, // uncomment to provide a custom "pick from" dialog.
// uncomment to provide a custom "pick from" dialog. // mediaPickSettingSelector: _selectMediaPickSetting,
// cameraPickSettingSelector: _selectCameraPickSetting, // uncomment to provide a custom "pick from" dialog.
// cameraPickSettingSelector: _selectCameraPickSetting,
),
showAlignmentButtons: true, showAlignmentButtons: true,
); );
if (kIsWeb) { if (kIsWeb) {
toolbar = QuillToolbar.basic( toolbar = QuillToolbar.basic(
controller: _controller!, controller: _controller!,
onImagePickCallback: _onImagePickCallback, embedButtons: FlutterQuillEmbeds.buttons(
webImagePickImpl: _webImagePickImpl, onImagePickCallback: _onImagePickCallback,
webImagePickImpl: _webImagePickImpl,
),
showAlignmentButtons: true, showAlignmentButtons: true,
); );
} }
if (_isDesktop()) { if (_isDesktop()) {
toolbar = QuillToolbar.basic( toolbar = QuillToolbar.basic(
controller: _controller!, controller: _controller!,
onImagePickCallback: _onImagePickCallback, embedButtons: FlutterQuillEmbeds.buttons(
filePickImpl: openFileSystemPickerForDesktop, onImagePickCallback: _onImagePickCallback,
filePickImpl: openFileSystemPickerForDesktop,
),
showAlignmentButtons: true, showAlignmentButtons: true,
); );
} }
@ -386,37 +396,42 @@ class _HomePageState extends State<HomePage> {
controller.replaceText(index, length, block, null); controller.replaceText(index, length, block, null);
} }
} }
}
Widget customElementsEmbedBuilder( class NotesEmbedBuilder implements EmbedBuilder {
BuildContext context, NotesEmbedBuilder({required this.addEditNote});
QuillController controller,
CustomBlockEmbed block, Future<void> Function(BuildContext context, {Document? document}) addEditNote;
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit, @override
) { String get key => 'notes';
switch (block.type) {
case 'notes': @override
final notes = NotesBlockEmbed(block.data).document; Widget build(
BuildContext context,
return Material( QuillController controller,
color: Colors.transparent, Embed node,
child: ListTile( bool readOnly,
title: Text( void Function(GlobalKey<State<StatefulWidget>> videoContainerKey)?
notes.toPlainText().replaceAll('\n', ' '), onVideoInit) {
maxLines: 3, final notes = NotesBlockEmbed(node.value.data).document;
overflow: TextOverflow.ellipsis,
), return Material(
leading: const Icon(Icons.notes), color: Colors.transparent,
onTap: () => _addEditNote(context, document: notes), child: ListTile(
shape: RoundedRectangleBorder( title: Text(
borderRadius: BorderRadius.circular(10), notes.toPlainText().replaceAll('\n', ' '),
side: const BorderSide(color: Colors.grey), maxLines: 3,
), overflow: TextOverflow.ellipsis,
), ),
); leading: const Icon(Icons.notes),
default: onTap: () => addEditNote(context, document: notes),
return const SizedBox(); shape: RoundedRectangleBorder(
} borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
),
);
} }
} }

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

@ -3,6 +3,7 @@ library universal_ui;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_quill/flutter_quill.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:universal_html/html.dart' as html;
import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart'; import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart';
@ -26,66 +27,72 @@ class UniversalUI {
var ui = UniversalUI(); var ui = UniversalUI();
Widget defaultEmbedBuilderWeb( class ImageEmbedBuilderWeb implements EmbedBuilder {
BuildContext context, @override
QuillController controller, String get key => BlockEmbed.imageType;
Embed node,
bool readOnly, @override
void Function(GlobalKey videoContainerKey)? onVideoInit, Widget build(BuildContext context, QuillController controller, Embed node,
) { bool readOnly, void Function(GlobalKey videoContainerKey)? onVideoInit) {
switch (node.value.type) { final imageUrl = node.value.data;
case BlockEmbed.imageType: if (isImageBase64(imageUrl)) {
final imageUrl = node.value.data; // TODO: handle imageUrl of base64
if (isImageBase64(imageUrl)) { return const SizedBox();
// TODO: handle imageUrl of base64 }
return const SizedBox(); final size = MediaQuery.of(context).size;
} UniversalUI().platformViewRegistry.registerViewFactory(
final size = MediaQuery.of(context).size; imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
UniversalUI().platformViewRegistry.registerViewFactory( return Padding(
imageUrl, (viewId) => html.ImageElement()..src = imageUrl); padding: EdgeInsets.only(
return Padding( right: ResponsiveWidget.isMediumScreen(context)
padding: EdgeInsets.only( ? size.width * 0.5
right: ResponsiveWidget.isMediumScreen(context) : (ResponsiveWidget.isLargeScreen(context))
? size.width * 0.5 ? size.width * 0.75
: (ResponsiveWidget.isLargeScreen(context)) : size.width * 0.2,
? size.width * 0.75 ),
: size.width * 0.2, child: SizedBox(
), height: MediaQuery.of(context).size.height * 0.45,
child: SizedBox( child: HtmlElementView(
height: MediaQuery.of(context).size.height * 0.45, viewType: imageUrl,
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) { class VideoEmbedBuilderWeb implements EmbedBuilder {
videoUrl = 'https://www.youtube.com/embed/$youtubeID'; @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( UniversalUI().platformViewRegistry.registerViewFactory(
videoUrl, videoUrl,
(id) => html.IFrameElement() (id) => html.IFrameElement()
..width = MediaQuery.of(context).size.width.toString() ..width = MediaQuery.of(context).size.width.toString()
..height = MediaQuery.of(context).size.height.toString() ..height = MediaQuery.of(context).size.height.toString()
..src = videoUrl ..src = videoUrl
..style.border = 'none'); ..style.border = 'none');
return SizedBox( return SizedBox(
height: 500, height: 500,
child: HtmlElementView( child: HtmlElementView(
viewType: videoUrl, 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.',
);
} }
} }
List<EmbedBuilder> get defaultEmbedBuildersWeb => [
ImageEmbedBuilderWeb(),
VideoEmbedBuilderWeb(),
];

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

@ -34,6 +34,12 @@ dependencies:
file_picker: ^4.6.1 file_picker: ^4.6.1
flutter_quill: flutter_quill:
path: ../ path: ../
flutter_quill_extensions:
path: ../flutter_quill_extensions
dependency_overrides:
flutter_quill:
path: ../
dev_dependencies: dev_dependencies:
flutter_test: 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/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../../models/themes/quill_icon_theme.dart'; import 'package:flutter_quill/translations.dart';
import '../../translations/toolbar.i18n.dart';
import '../controller.dart'; import '../embed_types.dart';
import '../toolbar.dart'; import 'image_video_utils.dart';
class CameraButton extends StatelessWidget { class CameraButton extends StatelessWidget {
const CameraButton({ const CameraButton({

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

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

@ -2,15 +2,13 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embeddable.dart'; import 'package:flutter_quill/translations.dart';
import '../../models/rules/insert.dart'; import 'package:flutter_quill/extensions.dart';
import '../../models/themes/quill_dialog_theme.dart';
import '../../translations/toolbar.i18n.dart'; import '../embed_types.dart';
import '../../utils/platform.dart';
import '../controller.dart';
import '../toolbar.dart';
class LinkDialog extends StatefulWidget { class LinkDialog extends StatefulWidget {
const LinkDialog({this.dialogTheme, this.link, Key? key}) : super(key: key); 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 { class ImageVideoUtils {
static Future<MediaPickSetting?> selectMediaPickSetting( static Future<MediaPickSetting?> selectMediaPickSetting(
BuildContext context, BuildContext context,

@ -1,11 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../../models/documents/nodes/embeddable.dart'; import '../embed_types.dart';
import '../../models/themes/quill_dialog_theme.dart'; import 'image_video_utils.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
class VideoButton extends StatelessWidget { class VideoButton extends StatelessWidget {
const VideoButton({ 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 'dart:io' as io;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:string_validator/string_validator.dart';
import '../../models/documents/attribute.dart'; import '../utils.dart';
import '../../models/documents/style.dart';
import '../controller.dart';
const List<String> imageFileExtensions = [ const List<String> imageFileExtensions = [
'.jpeg', '.jpeg',
@ -19,10 +17,6 @@ const List<String> imageFileExtensions = [
'.heic' '.heic'
]; ];
bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl);
}
String getImageStyleString(QuillController controller) { String getImageStyleString(QuillController controller) {
final String? s = controller final String? s = controller
.getAllSelectionStyles() .getAllSelectionStyles()

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

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

@ -1,10 +1,9 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart'; import 'package:youtube_player_flutter_quill/youtube_player_flutter_quill.dart';
import '../default_styles.dart';
class YoutubeVideoApp extends StatefulWidget { class YoutubeVideoApp extends StatefulWidget {
const YoutubeVideoApp( const YoutubeVideoApp(
{required this.videoUrl, required this.context, required this.readOnly}); {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/controller.dart';
export 'src/widgets/default_styles.dart'; export 'src/widgets/default_styles.dart';
export 'src/widgets/editor.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/link.dart' show LinkActionPickerDelegate, LinkMenuAction;
export 'src/widgets/style_widgets/style_widgets.dart'; export 'src/widgets/style_widgets/style_widgets.dart';
export 'src/widgets/toolbar.dart'; export 'src/widgets/toolbar.dart';

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

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

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

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

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:i18n_extension/i18n_widget.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 '../translations/toolbar.i18n.dart';
import '../utils/font.dart'; import '../utils/font.dart';
import 'controller.dart'; import 'controller.dart';
import 'embeds.dart';
import 'toolbar/arrow_indicated_button_list.dart'; import 'toolbar/arrow_indicated_button_list.dart';
import 'toolbar/camera_button.dart';
import 'toolbar/clear_format_button.dart'; import 'toolbar/clear_format_button.dart';
import 'toolbar/color_button.dart'; import 'toolbar/color_button.dart';
import 'toolbar/formula_button.dart';
import 'toolbar/history_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/indent_button.dart';
import 'toolbar/link_style_button.dart'; import 'toolbar/link_style_button.dart';
import 'toolbar/quill_font_family_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/select_header_style_button.dart';
import 'toolbar/toggle_check_list_button.dart'; import 'toolbar/toggle_check_list_button.dart';
import 'toolbar/toggle_style_button.dart'; import 'toolbar/toggle_style_button.dart';
import 'toolbar/video_button.dart';
export 'toolbar/clear_format_button.dart'; export 'toolbar/clear_format_button.dart';
export 'toolbar/color_button.dart'; export 'toolbar/color_button.dart';
export 'toolbar/history_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/indent_button.dart';
export 'toolbar/link_style_button.dart'; export 'toolbar/link_style_button.dart';
export 'toolbar/quill_font_size_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/select_header_style_button.dart';
export 'toolbar/toggle_check_list_button.dart'; export 'toolbar/toggle_check_list_button.dart';
export 'toolbar/toggle_style_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. // The default size of the icon of a button.
const double kDefaultIconSize = 18; const double kDefaultIconSize = 18;
@ -69,7 +50,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
this.toolbarSectionSpacing = 4, this.toolbarSectionSpacing = 4,
this.multiRowsDisplay = true, this.multiRowsDisplay = true,
this.color, this.color,
this.filePickImpl,
this.customButtons = const [], this.customButtons = const [],
this.locale, this.locale,
Key? key, Key? key,
@ -108,19 +88,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
bool showUndo = true, bool showUndo = true,
bool showRedo = true, bool showRedo = true,
bool multiRowsDisplay = true, bool multiRowsDisplay = true,
bool showImageButton = true,
bool showVideoButton = true,
bool showFormulaButton = false,
bool showCameraButton = true,
bool showDirection = false, bool showDirection = false,
bool showSearchButton = true, bool showSearchButton = true,
OnImagePickCallback? onImagePickCallback,
OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector,
MediaPickSettingSelector? cameraPickSettingSelector,
FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl,
List<QuillCustomButton> customButtons = const [], List<QuillCustomButton> customButtons = const [],
///Map of font sizes in string ///Map of font sizes in string
@ -129,6 +98,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
///Map of font families in string ///Map of font families in string
Map<String, String>? fontFamilyValues, 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] ///The theme to use for the icons in the toolbar, uses type [QuillIconTheme]
QuillIconTheme? iconTheme, QuillIconTheme? iconTheme,
@ -153,8 +125,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
showColorButton || showColorButton ||
showBackgroundColorButton || showBackgroundColorButton ||
showClearFormat || showClearFormat ||
onImagePickCallback != null || embedButtons?.isNotEmpty == true,
onVideoPickCallback != null,
showAlignmentButtons || showDirection, showAlignmentButtons || showDirection,
showLeftAlignment, showLeftAlignment,
showCenterAlignment, showCenterAlignment,
@ -330,56 +301,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
controller: controller, controller: controller,
iconTheme: iconTheme, iconTheme: iconTheme,
), ),
if (showImageButton) if (embedButtons != null)
ImageButton( for (final builder in embedButtons)
icon: Icons.image, builder(controller, toolbarIconSize, iconTheme, dialogTheme),
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 (showDividers && if (showDividers &&
isButtonGroupShown[0] && isButtonGroupShown[0] &&
(isButtonGroupShown[1] || (isButtonGroupShown[1] ||
@ -554,8 +478,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
/// is given. /// is given.
final Color? color; final Color? color;
final FilePickImpl? filePickImpl;
/// The locale to use for the editor toolbar, defaults to system locale /// The locale to use for the editor toolbar, defaults to system locale
/// More https://github.com/singerdmx/flutter-quill#translation /// More https://github.com/singerdmx/flutter-quill#translation
final Locale? locale; 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 collection: ^1.16.0
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flutter_keyboard_visibility: ^5.2.0 flutter_keyboard_visibility: ^5.2.0
image_picker: ^0.8.5+3
photo_view: ^0.14.0
quiver: ^3.1.0 quiver: ^3.1.0
string_validator: ^0.3.0
tuple: ^2.0.0 tuple: ^2.0.0
url_launcher: ^6.1.2 url_launcher: ^6.1.2
pedantic: ^1.11.1 pedantic: ^1.11.1
video_player: ^2.4.2
characters: ^1.2.0 characters: ^1.2.0
youtube_player_flutter_quill: ^8.2.2
diff_match_patch: ^0.4.1 diff_match_patch: ^0.4.1
i18n_extension: ^5.0.1 i18n_extension: ^5.0.1
gallery_saver: ^2.3.2
device_info_plus: ^4.0.0 device_info_plus: ^4.0.0
platform: ^3.1.0 platform: ^3.1.0
math_keyboard: ^0.1.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save