diff --git a/CHANGELOG.md b/CHANGELOG.md index 7769f54a..e578affd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# [3.9.6] +* Apply locale to QuillEditor(contents). + +# [3.9.5] +* Fix image pasting. + # [3.9.4] * Hiding dialog after selecting action for image. diff --git a/README.md b/README.md index 371c23ef..5c1520cf 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follo The package offers translations for the quill toolbar, it will follow the system locale unless you set your own locale with: ``` QuillToolbar(locale: Locale('fr'), ...) +QuillEditor(locale: Locale('fr'), ...) ``` Currently, translations are available for these locales: * `Locale('en')` diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 5a8b840e..665a5416 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -270,7 +270,7 @@ class Document { for (var i = 0; i < ops.length; i++) { final op = ops[i]; res.push(op); - _autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'video'); + _autoAppendNewlineAfterEmbeddable(i, ops, op, res, BlockEmbed.videoType); } return res; } diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 9a200927..f1db6318 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -2,6 +2,7 @@ import 'package:tuple/tuple.dart'; import '../../models/documents/document.dart'; import '../documents/attribute.dart'; +import '../documents/nodes/embeddable.dart'; import '../documents/style.dart'; import '../quill_delta.dart'; import 'rule.dart'; @@ -239,6 +240,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { } /// Handles all format operations which manipulate embeds. +/// This rule wraps line breaks around video, not image. class InsertEmbedsRule extends InsertRule { const InsertEmbedsRule(); @@ -249,6 +251,12 @@ class InsertEmbedsRule extends InsertRule { return null; } + assert(data is Map); + + if (!(data as Map).containsKey(BlockEmbed.videoType)) { + return null; + } + final delta = Delta()..retain(index + (len ?? 0)); final itr = DeltaIterator(document); final prev = itr.skip(index), cur = itr.next(); diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 13766063..3e418287 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:i18n_extension/i18n_widget.dart'; import 'package:tuple/tuple.dart'; import '../models/documents/document.dart'; @@ -169,6 +170,7 @@ class QuillEditor extends StatefulWidget { this.embedBuilder = defaultEmbedBuilder, this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.customStyleBuilder, + this.locale, this.floatingCursorDisabled = false, Key? key}) : super(key: key); @@ -339,6 +341,10 @@ class QuillEditor extends StatefulWidget { final EmbedBuilder embedBuilder; final CustomStyleBuilder? customStyleBuilder; + /// The locale to use for the editor toolbar, defaults to system locale + /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar + final Locale? locale; + /// Delegate function responsible for showing menu with link actions on /// mobile platforms (iOS, Android). /// @@ -452,10 +458,12 @@ class QuillEditorState extends State floatingCursorDisabled: widget.floatingCursorDisabled, ); - final editor = _selectionGestureDetectorBuilder.build( - behavior: HitTestBehavior.translucent, - child: child, - ); + final editor = I18n( + initialLocale: widget.locale, + child: _selectionGestureDetectorBuilder.build( + behavior: HitTestBehavior.translucent, + child: child, + )); if (kIsWeb) { // Intercept RawKeyEvent on Web to prevent it from propagating to parents diff --git a/lib/src/widgets/embeds/default_embed_builder.dart b/lib/src/widgets/embeds/default_embed_builder.dart index 2a27b455..dab275c5 100644 --- a/lib/src/widgets/embeds/default_embed_builder.dart +++ b/lib/src/widgets/embeds/default_embed_builder.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -7,8 +5,8 @@ import 'package:gallery_saver/gallery_saver.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 '../../models/documents/style.dart'; import '../../translations/toolbar.i18n.dart'; import '../../utils/platform.dart'; import '../../utils/string.dart'; @@ -24,7 +22,7 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, Tuple2? _widthHeight; switch (node.value.type) { - case 'image': + case BlockEmbed.imageType: final imageUrl = standardizeImageUrl(node.value.data); var image; final style = node.style.attributes['style']; @@ -76,9 +74,10 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, final _screenSize = MediaQuery.of(context).size; return ImageResizer( onImageResize: (w, h) { - final res = _getImageNode(controller); + final res = getImageNode( + controller, controller.selection.start); final attr = replaceStyleString( - _getImageStyleString(controller), w, h); + getImageStyleString(controller), w, h); controller.formatText( res.item1, 1, StyleAttribute(attr)); }, @@ -94,10 +93,12 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, color: Colors.cyanAccent, text: 'Copy'.i18n, onPressed: () { - final imageNode = _getImageNode(controller).item2; + final imageNode = + getImageNode(controller, controller.selection.start) + .item2; final imageUrl = imageNode.value.data; controller.copiedImageUrl = - Tuple2(imageUrl, _getImageStyleString(controller)); + Tuple2(imageUrl, getImageStyleString(controller)); Navigator.pop(context); }, ); @@ -106,7 +107,9 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, color: Colors.red.shade200, text: 'Remove'.i18n, onPressed: () { - final offset = _getImageNode(controller).item1; + final offset = + getImageNode(controller, controller.selection.start) + .item1; controller.replaceText(offset, 1, '', TextSelection.collapsed(offset: offset)); Navigator.pop(context); @@ -131,7 +134,7 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, // We provide option menu for mobile platform excluding base64 image return _menuOptionsForReadonlyImage(context, imageUrl, image); - case 'video': + case BlockEmbed.videoType: final videoUrl = node.value.data; if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { return YoutubeVideoApp( @@ -147,30 +150,6 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, } } -String _getImageStyleString(QuillController controller) { - final String? s = controller - .getAllSelectionStyles() - .firstWhere((s) => s.attributes.containsKey(Attribute.style.key), - orElse: () => Style()) - .attributes[Attribute.style.key] - ?.value; - return s ?? ''; -} - -Tuple2 _getImageNode(QuillController controller) { - var offset = controller.selection.start; - var imageNode = controller.queryNode(offset); - if (imageNode == null || !(imageNode is leaf.Embed)) { - offset = max(0, offset - 1); - imageNode = controller.queryNode(offset); - } - if (imageNode != null && imageNode is leaf.Embed) { - return Tuple2(offset, imageNode); - } - - return throw 'Image node not found by offset $offset'; -} - Widget _menuOptionsForReadonlyImage( BuildContext context, String imageUrl, Image image) { return GestureDetector( diff --git a/lib/src/widgets/embeds/image.dart b/lib/src/widgets/embeds/image.dart index 8faf3d43..c7e9f9e3 100644 --- a/lib/src/widgets/embeds/image.dart +++ b/lib/src/widgets/embeds/image.dart @@ -1,14 +1,45 @@ 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'; 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() + .firstWhere((s) => s.attributes.containsKey(Attribute.style.key), + orElse: () => Style()) + .attributes[Attribute.style.key] + ?.value; + return s ?? ''; +} + Image imageByUrl(String imageUrl, {double? width, double? height, diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index d4b26f71..473eea10 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -26,6 +26,7 @@ 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'; @@ -927,8 +928,10 @@ class RawEditorState extends EditorState widget.controller .replaceText(index, length, BlockEmbed.image(copied.item1), null); if (copied.item2.isNotEmpty) { - widget.controller - .formatText(index + 1, 1, StyleAttribute(copied.item2)); + widget.controller.formatText( + getImageNode(widget.controller, index + 1).item1, + 1, + StyleAttribute(copied.item2)); } widget.controller.copiedImageUrl = null; await Clipboard.setData(const ClipboardData(text: '')); diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 9bb93777..454fa415 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -424,12 +424,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final FilePickImpl? filePickImpl; - ///The locale to use for the editor toolbar, defaults to system locale - ///Currently the supported locales are: - /// * Locale('en') - /// * Locale('de') - /// * Locale('fr') - /// * Locale('zh', 'CN') + /// The locale to use for the editor toolbar, defaults to system locale /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar final Locale? locale; diff --git a/pubspec.yaml b/pubspec.yaml index 7fff8716..3737784f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.4 +version: 3.9.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill @@ -30,7 +30,7 @@ dependencies: characters: ^1.1.0 youtube_player_flutter: ^8.0.0 diff_match_patch: ^0.4.1 - i18n_extension: ^4.1.3 + i18n_extension: ^4.2.0 gallery_saver: ^2.3.2 dev_dependencies: