From bdc976828cf20fad61ac55ca86e911806f6367ab Mon Sep 17 00:00:00 2001 From: LucazzP Date: Mon, 18 Jul 2022 17:34:44 -0300 Subject: [PATCH 1/2] Added portuguese translations for image resizer --- lib/src/translations/toolbar.i18n.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 077176813..41d86c030 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -273,10 +273,10 @@ extension Localization on String { 'Saved': 'Salvo', 'Text': 'Texto', 'What is entered is not a link': 'O link inserido não é válido', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', + 'Resize': 'Redimencionar', + 'Width': 'Largura', + 'Height': 'Altura', + 'Size': 'Tamanho', }, 'pl': { 'Paste a link': 'Wklej link', From 846209f9ff9ef161c48a7192af06d60bf946fb35 Mon Sep 17 00:00:00 2001 From: LucazzP Date: Wed, 20 Jul 2022 23:18:39 -0300 Subject: [PATCH 2/2] Added CustomBlockEmbed and customElementsEmbedBuilder --- example/ios/Runner/Info.plist | 2 + example/lib/pages/home_page.dart | 140 +++++++++++++++--- lib/flutter_quill.dart | 1 + .../models/documents/nodes/embeddable.dart | 17 +++ lib/src/utils/embeds.dart | 19 +++ lib/src/widgets/delegate.dart | 8 + lib/src/widgets/editor.dart | 30 +++- .../widgets/embeds/default_embed_builder.dart | 7 +- lib/src/widgets/embeds/image.dart | 17 --- lib/src/widgets/raw_editor.dart | 4 +- 10 files changed, 199 insertions(+), 46 deletions(-) create mode 100644 lib/src/utils/embeds.dart diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 9f086d9df..55f5c3083 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 3bc1f2caf..8c150b61b 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -61,7 +61,12 @@ class _HomePageState extends State { title: const Text( 'Flutter Quill', ), - actions: [], + actions: [ + IconButton( + onPressed: () => _addEditNote(context), + icon: const Icon(Icons.note_add), + ), + ], ), drawer: Container( constraints: @@ -92,28 +97,30 @@ class _HomePageState extends State { Widget _buildWelcomeEditor(BuildContext context) { var quillEditor = QuillEditor( - controller: _controller!, - scrollController: ScrollController(), - scrollable: true, - focusNode: _focusNode, - autoFocus: false, - readOnly: false, - placeholder: 'Add content', - expands: false, - padding: EdgeInsets.zero, - customStyles: DefaultStyles( - h1: DefaultTextBlockStyle( - const TextStyle( - fontSize: 32, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - const Tuple2(16, 0), - const Tuple2(0, 0), - null), - sizeSmall: const TextStyle(fontSize: 9), - )); + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + expands: false, + padding: EdgeInsets.zero, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + const Tuple2(16, 0), + const Tuple2(0, 0), + null), + sizeSmall: const TextStyle(fontSize: 9), + ), + customElementsEmbedBuilder: customElementsEmbedBuilder, + ); if (kIsWeb) { quillEditor = QuillEditor( controller: _controller!, @@ -304,4 +311,91 @@ class _HomePageState extends State { ), ); } + + Future _addEditNote(BuildContext context, {Document? document}) async { + final isEditing = document != null; + final quillEditorController = QuillController( + document: document ?? Document(), + selection: const TextSelection.collapsed(offset: 0), + ); + + await showDialog( + context: context, + builder: (context) => AlertDialog( + titlePadding: const EdgeInsets.only(left: 16, top: 8), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${isEditing ? 'Edit' : 'Add'} note'), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close), + ) + ], + ), + content: QuillEditor.basic( + controller: quillEditorController, + readOnly: false, + ), + ), + ); + + if (quillEditorController.document.isEmpty()) return; + + final block = BlockEmbed.custom( + NotesBlockEmbed.fromDocument(quillEditorController.document), + ); + final controller = _controller!; + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + + if (isEditing) { + final offset = getEmbedNode(controller, controller.selection.start).item1; + controller.replaceText( + offset, 1, block, TextSelection.collapsed(offset: offset)); + } else { + 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 NotesBlockEmbed extends CustomBlockEmbed { + const NotesBlockEmbed(String value) : super('notes', value); + + static NotesBlockEmbed fromDocument(Document document) => + NotesBlockEmbed(jsonEncode(document.toDelta().toJson())); + + Document get document => Document.fromJson(jsonDecode(data)); } diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index ec8a81636..4ff26faa7 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -9,6 +9,7 @@ export 'src/models/quill_delta.dart'; export 'src/models/themes/quill_custom_icon.dart'; export 'src/models/themes/quill_dialog_theme.dart'; export 'src/models/themes/quill_icon_theme.dart'; +export 'src/utils/embeds.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart'; diff --git a/lib/src/models/documents/nodes/embeddable.dart b/lib/src/models/documents/nodes/embeddable.dart index fa14d50a2..61d5e1159 100644 --- a/lib/src/models/documents/nodes/embeddable.dart +++ b/lib/src/models/documents/nodes/embeddable.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + /// An object which can be embedded into a Quill document. /// /// See also: @@ -35,4 +37,19 @@ class BlockEmbed extends Embeddable { static const String videoType = 'video'; static BlockEmbed video(String videoUrl) => BlockEmbed(videoType, videoUrl); + + static const String customType = 'custom'; + static BlockEmbed custom(CustomBlockEmbed customBlock) => + BlockEmbed(customType, customBlock.toJsonString()); +} + +class CustomBlockEmbed extends BlockEmbed { + const CustomBlockEmbed(String type, String data) : super(type, data); + + String toJsonString() => jsonEncode(toJson()); + + static CustomBlockEmbed fromJsonString(String data) { + final embeddable = Embeddable.fromJson(jsonDecode(data)); + return CustomBlockEmbed(embeddable.type, embeddable.data); + } } diff --git a/lib/src/utils/embeds.dart b/lib/src/utils/embeds.dart new file mode 100644 index 000000000..f7c3c9d09 --- /dev/null +++ b/lib/src/utils/embeds.dart @@ -0,0 +1,19 @@ +import 'dart:math'; + +import 'package:tuple/tuple.dart'; + +import '../../flutter_quill.dart'; + +Tuple2 getEmbedNode(QuillController controller, int offset) { + var offset = controller.selection.start; + var imageNode = controller.queryNode(offset); + if (imageNode == null || !(imageNode is Embed)) { + offset = max(0, offset - 1); + imageNode = controller.queryNode(offset); + } + if (imageNode != null && imageNode is Embed) { + return Tuple2(offset, imageNode); + } + + return throw 'Embed node not found by offset $offset'; +} diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index 2bc696df9..6fdd81afa 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -14,6 +14,14 @@ 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]. diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 74faf5b63..e5b0916ea 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -11,6 +11,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/style.dart'; import '../utils/platform.dart'; import 'box.dart'; @@ -168,6 +169,7 @@ class QuillEditor extends StatefulWidget { this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, this.embedBuilder = defaultEmbedBuilder, + this.customElementsEmbedBuilder, this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.customStyleBuilder, this.locale, @@ -340,6 +342,7 @@ class QuillEditor extends StatefulWidget { onSingleLongTapEnd; final EmbedBuilder embedBuilder; + final CustomEmbedBuilder? customElementsEmbedBuilder; final CustomStyleBuilder? customStyleBuilder; /// The locale to use for the editor toolbar, defaults to system locale @@ -458,7 +461,32 @@ class QuillEditorState extends State keyboardAppearance: widget.keyboardAppearance, enableInteractiveSelection: widget.enableInteractiveSelection, scrollPhysics: widget.scrollPhysics, - embedBuilder: widget.embedBuilder, + embedBuilder: ( + context, + controller, + node, + 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, + ); + } + return widget.embedBuilder( + context, + controller, + node, + readOnly, + onVideoInit, + ); + }, linkActionPickerDelegate: widget.linkActionPickerDelegate, customStyleBuilder: widget.customStyleBuilder, floatingCursorDisabled: widget.floatingCursorDisabled, diff --git a/lib/src/widgets/embeds/default_embed_builder.dart b/lib/src/widgets/embeds/default_embed_builder.dart index 519a39c54..1c5b274e1 100644 --- a/lib/src/widgets/embeds/default_embed_builder.dart +++ b/lib/src/widgets/embeds/default_embed_builder.dart @@ -8,6 +8,7 @@ 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'; @@ -79,7 +80,7 @@ Widget defaultEmbedBuilder( final _screenSize = MediaQuery.of(context).size; return ImageResizer( onImageResize: (w, h) { - final res = getImageNode( + final res = getEmbedNode( controller, controller.selection.start); final attr = replaceStyleString( getImageStyleString(controller), w, h); @@ -99,7 +100,7 @@ Widget defaultEmbedBuilder( text: 'Copy'.i18n, onPressed: () { final imageNode = - getImageNode(controller, controller.selection.start) + getEmbedNode(controller, controller.selection.start) .item2; final imageUrl = imageNode.value.data; controller.copiedImageUrl = @@ -113,7 +114,7 @@ Widget defaultEmbedBuilder( text: 'Remove'.i18n, onPressed: () { final offset = - getImageNode(controller, controller.selection.start) + getEmbedNode(controller, controller.selection.start) .item1; controller.replaceText(offset, 1, '', TextSelection.collapsed(offset: offset)); diff --git a/lib/src/widgets/embeds/image.dart b/lib/src/widgets/embeds/image.dart index 6bbedd188..8f2d70af5 100644 --- a/lib/src/widgets/embeds/image.dart +++ b/lib/src/widgets/embeds/image.dart @@ -1,14 +1,11 @@ import 'dart:convert'; import 'dart:io' as io; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:tuple/tuple.dart'; import '../../models/documents/attribute.dart'; -import '../../models/documents/nodes/leaf.dart'; import '../../models/documents/style.dart'; import '../controller.dart'; @@ -26,20 +23,6 @@ bool isImageBase64(String imageUrl) { return !imageUrl.startsWith('http') && isBase64(imageUrl); } -Tuple2 getImageNode(QuillController controller, int offset) { - var offset = controller.selection.start; - var imageNode = controller.queryNode(offset); - if (imageNode == null || !(imageNode is Embed)) { - offset = max(0, offset - 1); - imageNode = controller.queryNode(offset); - } - if (imageNode != null && imageNode is Embed) { - return Tuple2(offset, imageNode); - } - - return throw 'Image node not found by offset $offset'; -} - String getImageStyleString(QuillController controller) { final String? s = controller .getAllSelectionStyles() diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 0d3f0a256..e6f917bde 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -19,6 +19,7 @@ import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; import '../utils/delta.dart'; +import '../utils/embeds.dart'; import '../utils/platform.dart'; import 'controller.dart'; import 'cursor.dart'; @@ -26,7 +27,6 @@ import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; import 'embeds/default_embed_builder.dart'; -import 'embeds/image.dart'; import 'keyboard_listener.dart'; import 'link.dart'; import 'proxy.dart'; @@ -996,7 +996,7 @@ class RawEditorState extends EditorState .replaceText(index, length, BlockEmbed.image(copied.item1), null); if (copied.item2.isNotEmpty) { widget.controller.formatText( - getImageNode(widget.controller, index + 1).item1, + getEmbedNode(widget.controller, index + 1).item1, 1, StyleAttribute(copied.item2)); }