From a4b6641b5714eeca5c10ee2fc66cc224a3aa9e8c Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 31 Jan 2022 13:23:42 -0800 Subject: [PATCH] Support resizing image --- lib/src/models/documents/nodes/line.dart | 4 ++- lib/src/models/rules/format.dart | 23 ++++++++++++++- lib/src/models/rules/rule.dart | 1 + lib/src/utils/string.dart | 26 +++++++++++++++++ .../widgets/embeds/default_embed_builder.dart | 24 +++++++++++++--- lib/src/widgets/embeds/image_resizer.dart | 28 ++++++++++++++----- 6 files changed, 93 insertions(+), 13 deletions(-) diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index 9059b321..83c07324 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -134,7 +134,9 @@ class Line extends Container { _format(style); } else { // Otherwise forward to children as it's an inline format update. - assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE)); + assert(style.values.every((attr) => + attr.scope == AttributeScope.INLINE || + attr.scope == AttributeScope.IGNORE)); assert(index + local != thisLength); super.retain(index, local, style); } diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index e1833b7c..0424903a 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -136,7 +136,7 @@ class FormatLinkAtCaretPositionRule extends FormatRule { } } -/// Produces Delta with inline-level attributes applied too all characters +/// Produces Delta with inline-level attributes applied to all characters /// except newlines. class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); @@ -176,3 +176,24 @@ class ResolveInlineFormatRule extends FormatRule { return delta; } } + +/// Produces Delta with attributes applied to image leaf node +class ResolveImageFormatRule extends FormatRule { + const ResolveImageFormatRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute == null || attribute.key != Attribute.style.key) { + return null; + } + + assert(len == 1 && data == null); + + final delta = Delta() + ..retain(index) + ..retain(1, attribute.toJson()); + + return delta; + } +} diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index d29bc9a1..de9db513 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -37,6 +37,7 @@ class Rules { const FormatLinkAtCaretPositionRule(), const ResolveLineFormatRule(), const ResolveInlineFormatRule(), + const ResolveImageFormatRule(), const InsertEmbedsRule(), const AutoExitBlockRule(), const PreserveBlockStyleOnInsertRule(), diff --git a/lib/src/utils/string.dart b/lib/src/utils/string.dart index e3d8b7e0..d848b11e 100644 --- a/lib/src/utils/string.dart +++ b/lib/src/utils/string.dart @@ -17,6 +17,32 @@ Map parseKeyValuePairs(String s, Set targetKeys) { return result; } +String replaceStyleString(String? s, double width, double height) { + s ??= ''; + final result = {}; + final pairs = s.split(';'); + for (final pair in pairs) { + final _index = pair.indexOf(':'); + if (_index < 0) { + continue; + } + final _key = pair.substring(0, _index).trim(); + result[_key] = pair.substring(_index + 1).trim(); + } + + result['mobileWidth'] = width.toString(); + result['mobileHeight'] = height.toString(); + final sb = StringBuffer(); + for (final pair in result.entries) { + sb + ..write(pair.key) + ..write(': ') + ..write(pair.value) + ..write('; '); + } + return sb.toString(); +} + Alignment getAlignment(String? s) { const _defaultAlignment = Alignment.center; if (s == null) { diff --git a/lib/src/widgets/embeds/default_embed_builder.dart b/lib/src/widgets/embeds/default_embed_builder.dart index 4c3979b6..2d219b91 100644 --- a/lib/src/widgets/embeds/default_embed_builder.dart +++ b/lib/src/widgets/embeds/default_embed_builder.dart @@ -6,7 +6,9 @@ import 'package:flutter/material.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:tuple/tuple.dart'; +import '../../models/documents/attribute.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'; @@ -63,14 +65,28 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, text: 'Resize'.i18n, onPressed: () { Navigator.pop(context); - final res = _getImageNode(controller); showCupertinoModalPopup( context: context, builder: (context) { final _screenSize = MediaQuery.of(context).size; + final _style = controller + .getAllSelectionStyles() + .firstWhere( + (s) => s.attributes + .containsKey(Attribute.style.key), + orElse: () => Style()); + return ImageResizer( - imageNode: res.item2, - offset: res.item1, + onImageResize: (w, h) { + final res = _getImageNode(controller); + final attr = replaceStyleString( + _style.attributes[Attribute.style.key] + ?.value, + w, + h); + controller.formatText( + res.item1, 1, StyleAttribute(attr)); + }, imageWidth: _widthHeight?.item1, imageHeight: _widthHeight?.item2, maxWidth: _screenSize.width, @@ -106,7 +122,7 @@ Widget defaultEmbedBuilder(BuildContext context, QuillController controller, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), - children: [copyOption, removeOption]), + children: [resizeOption, copyOption, removeOption]), ); }); }, diff --git a/lib/src/widgets/embeds/image_resizer.dart b/lib/src/widgets/embeds/image_resizer.dart index 2decf7d2..527ab6be 100644 --- a/lib/src/widgets/embeds/image_resizer.dart +++ b/lib/src/widgets/embeds/image_resizer.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; -import '../../models/documents/nodes/leaf.dart'; import '../../translations/toolbar.i18n.dart'; class ImageResizer extends StatefulWidget { @@ -10,8 +10,7 @@ class ImageResizer extends StatefulWidget { required this.imageHeight, required this.maxWidth, required this.maxHeight, - required this.offset, - required this.imageNode, + required this.onImageResize, Key? key}) : super(key: key); @@ -19,8 +18,7 @@ class ImageResizer extends StatefulWidget { final double? imageHeight; final double maxWidth; final double maxHeight; - final int offset; - final Embed imageNode; + final Function(double, double) onImageResize; @override _ImageResizerState createState() => _ImageResizerState(); @@ -48,11 +46,12 @@ class _ImageResizerState extends State { child: Slider( value: _width, max: widget.maxWidth, - divisions: 100, + divisions: 1000, label: 'Width'.i18n, onChanged: (val) { setState(() { _width = val; + _resizeImage(); }); }, ), @@ -66,11 +65,12 @@ class _ImageResizerState extends State { child: Slider( value: _height, max: widget.maxHeight, - divisions: 100, + divisions: 1000, label: 'Height'.i18n, onChanged: (val) { setState(() { _height = val; + _resizeImage(); }); }, ), @@ -78,4 +78,18 @@ class _ImageResizerState extends State { ) ]); } + + bool _scheduled = false; + + void _resizeImage() { + if (_scheduled) { + return; + } + + _scheduled = true; + SchedulerBinding.instance!.addPostFrameCallback((_) { + widget.onImageResize(_width, _height); + _scheduled = false; + }); + } }