From 94c6a1dcbc1b97a2636bb73e55c8b086e9fcc324 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 3 Feb 2022 18:41:10 -0800 Subject: [PATCH] Fix image pasting --- lib/src/models/documents/document.dart | 2 +- lib/src/models/rules/insert.dart | 8 ++++ .../widgets/embeds/default_embed_builder.dart | 47 +++++-------------- lib/src/widgets/embeds/image.dart | 31 ++++++++++++ lib/src/widgets/raw_editor.dart | 7 ++- 5 files changed, 58 insertions(+), 37 deletions(-) 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/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: ''));