Image menu options for copy/remove

pull/635/head
X Code 3 years ago
parent cfbd3d2912
commit 35b53c7404
  1. 3
      example/lib/universal_ui/universal_ui.dart
  2. 17
      lib/src/widgets/controller.dart
  3. 4
      lib/src/widgets/delegate.dart
  4. 99
      lib/src/widgets/embeds/default_embed_builder.dart
  5. 43
      lib/src/widgets/embeds/image_resizer.dart
  6. 15
      lib/src/widgets/raw_editor.dart
  7. 7
      lib/src/widgets/text_line.dart

@ -25,7 +25,8 @@ class UniversalUI {
var ui = UniversalUI(); var ui = UniversalUI();
Widget defaultEmbedBuilderWeb(BuildContext context, Embed node, bool readOnly) { Widget defaultEmbedBuilderWeb(BuildContext context, QuillController controller,
Embed node, bool readOnly) {
switch (node.value.type) { switch (node.value.type) {
case 'image': case 'image':
final imageUrl = node.value.data; final imageUrl = node.value.data;

@ -1,11 +1,13 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart'; import '../models/documents/attribute.dart';
import '../models/documents/document.dart'; import '../models/documents/document.dart';
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 '../models/quill_delta.dart'; import '../models/quill_delta.dart';
import '../utils/delta.dart'; import '../utils/delta.dart';
@ -327,6 +329,21 @@ class QuillController extends ChangeNotifier {
extentOffset: math.min(selection.extentOffset, end)); extentOffset: math.min(selection.extentOffset, end));
} }
/// Given offset, find its leaf node in document
Leaf? queryNode(int offset) {
return document.querySegmentLeafNode(offset).item2;
}
/// Clipboard for image url
String? _copiedImageUrl;
String? getCopiedImageUrl() => _copiedImageUrl;
set copiedImageUrl(String? value) {
_copiedImageUrl = value;
Clipboard.setData(const ClipboardData(text: ''));
}
// Notify toolbar buttons directly with attributes // Notify toolbar buttons directly with attributes
Map<String, Attribute> toolbarButtonToggler = {}; Map<String, Attribute> toolbarButtonToggler = {};
} }

@ -5,8 +5,8 @@ import 'package:flutter/material.dart';
import '../../flutter_quill.dart'; import '../../flutter_quill.dart';
import 'text_selection.dart'; import 'text_selection.dart';
typedef EmbedBuilder = Widget Function( typedef EmbedBuilder = Widget Function(BuildContext context,
BuildContext context, Embed node, bool readOnly); QuillController controller, Embed node, bool readOnly);
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute); typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);

@ -1,3 +1,6 @@
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gallery_saver/gallery_saver.dart'; import 'package:gallery_saver/gallery_saver.dart';
@ -6,12 +9,14 @@ import '../../models/documents/nodes/leaf.dart' as leaf;
import '../../translations/toolbar.i18n.dart'; import '../../translations/toolbar.i18n.dart';
import '../../utils/platform.dart'; import '../../utils/platform.dart';
import '../../utils/string.dart'; import '../../utils/string.dart';
import '../controller.dart';
import 'image.dart'; import 'image.dart';
import 'image_resizer.dart';
import 'video_app.dart'; import 'video_app.dart';
import 'youtube_video_app.dart'; import 'youtube_video_app.dart';
Widget defaultEmbedBuilder( Widget defaultEmbedBuilder(BuildContext context, QuillController controller,
BuildContext context, leaf.Embed node, bool readOnly) { leaf.Embed node, bool readOnly) {
assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
switch (node.value.type) { switch (node.value.type) {
@ -40,34 +45,68 @@ Widget defaultEmbedBuilder(
image ??= imageByUrl(imageUrl); image ??= imageByUrl(imageUrl);
if (!readOnly && isMobile()) { if (!readOnly && isMobile()) {
// TODO: slider for width and height return GestureDetector(
// return GestureDetector( onTap: () {
// onTap: () { showDialog(
// showDialog( context: context,
// context: context, builder: (context) {
// builder: (context) => Padding( final resizeOption = _SimpleDialogItem(
// padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), icon: Icons.settings_outlined,
// child: SimpleDialog( color: Colors.lightBlueAccent,
// shape: const RoundedRectangleBorder( text: 'Resize'.i18n,
// borderRadius: onPressed: () {
// BorderRadius.all(Radius.circular(10))), Navigator.pop(context);
// children: [ showCupertinoModalPopup<void>(
// _SimpleDialogItem( context: context,
// icon: Icons.settings_outlined, builder: (context) {
// color: Colors.lightBlueAccent, return const ImageResizer();
// text: 'Resize'.i18n, });
// onPressed: () {}, },
// ), );
// _SimpleDialogItem( final copyOption = _SimpleDialogItem(
// icon: Icons.delete_forever_outlined, icon: Icons.copy_all_outlined,
// color: Colors.red.shade200, color: Colors.cyanAccent,
// text: 'Remove'.i18n, text: 'Copy'.i18n,
// onPressed: () {}, onPressed: () {
// ) var offset = controller.selection.start;
// ]), var imageNode = controller.queryNode(offset);
// )); if (imageNode == null || !(imageNode is leaf.Embed)) {
// }, offset = max(0, offset - 1);
// child: image); imageNode = controller.queryNode(offset);
}
if (imageNode != null && imageNode is leaf.Embed) {
final imageUrl = imageNode.value.data;
controller.copiedImageUrl = imageUrl;
}
Navigator.pop(context);
},
);
final removeOption = _SimpleDialogItem(
icon: Icons.delete_forever_outlined,
color: Colors.red.shade200,
text: 'Remove'.i18n,
onPressed: () {
var offset = controller.selection.start;
final imageNode = controller.queryNode(offset);
if (imageNode == null || !(imageNode is leaf.Embed)) {
offset = max(0, offset - 1);
}
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: [copyOption, removeOption]),
);
});
},
child: image);
} }
if (!readOnly || !isMobile() || isImageBase64(imageUrl)) { if (!readOnly || !isMobile() || isImageBase64(imageUrl)) {

@ -0,0 +1,43 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ImageResizer extends StatefulWidget {
const ImageResizer({Key? key}) : super(key: key);
@override
_ImageResizerState createState() => _ImageResizerState();
}
class _ImageResizerState extends State<ImageResizer> {
@override
Widget build(BuildContext context) {
return CupertinoActionSheet(actions: [
CupertinoActionSheetAction(
onPressed: () {},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
child: Slider(
value: 50,
max: 100,
divisions: 5,
onChanged: (val) {},
),
)),
),
CupertinoActionSheetAction(
onPressed: () {},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
child: Slider(
value: 10,
max: 100,
divisions: 5,
onChanged: (val) {},
),
)),
)
]);
}
}

@ -15,6 +15,7 @@ import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart'; import '../models/documents/attribute.dart';
import '../models/documents/document.dart'; import '../models/documents/document.dart';
import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart'; import '../models/documents/nodes/node.dart';
import '../models/documents/style.dart'; import '../models/documents/style.dart';
@ -880,6 +881,7 @@ class RawEditorState extends EditorState
@override @override
void copySelection(SelectionChangedCause cause) { void copySelection(SelectionChangedCause cause) {
widget.controller.copiedImageUrl = null;
_pastePlainText = widget.controller.getPlainText(); _pastePlainText = widget.controller.getPlainText();
_pasteStyle = widget.controller.getAllIndividualSelectionStyles(); _pasteStyle = widget.controller.getAllIndividualSelectionStyles();
// Copied straight from EditableTextState // Copied straight from EditableTextState
@ -904,8 +906,10 @@ class RawEditorState extends EditorState
@override @override
void cutSelection(SelectionChangedCause cause) { void cutSelection(SelectionChangedCause cause) {
widget.controller.copiedImageUrl = null;
_pastePlainText = widget.controller.getPlainText(); _pastePlainText = widget.controller.getPlainText();
_pasteStyle = widget.controller.getAllIndividualSelectionStyles(); _pasteStyle = widget.controller.getAllIndividualSelectionStyles();
// Copied straight from EditableTextState // Copied straight from EditableTextState
super.cutSelection(cause); super.cutSelection(cause);
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
@ -916,8 +920,19 @@ class RawEditorState extends EditorState
@override @override
Future<void> pasteText(SelectionChangedCause cause) async { Future<void> pasteText(SelectionChangedCause cause) async {
if (widget.controller.getCopiedImageUrl() != null) {
final index = textEditingValue.selection.baseOffset;
final length = textEditingValue.selection.extentOffset - index;
widget.controller.replaceText(index, length,
BlockEmbed.image(widget.controller.getCopiedImageUrl()!), null);
widget.controller.copiedImageUrl = null;
await Clipboard.setData(const ClipboardData(text: ''));
return;
}
// Copied straight from EditableTextState // Copied straight from EditableTextState
super.pasteText(cause); // ignore: unawaited_futures super.pasteText(cause); // ignore: unawaited_futures
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent); bringIntoView(textEditingValue.selection.extent);
hideToolbar(); hideToolbar();

@ -133,7 +133,8 @@ class _TextLineState extends State<TextLine> {
if (widget.line.hasEmbed && widget.line.childCount == 1) { if (widget.line.hasEmbed && widget.line.childCount == 1) {
// For video, it is always single child // For video, it is always single child
final embed = widget.line.children.single as Embed; final embed = widget.line.children.single as Embed;
return EmbedProxy(widget.embedBuilder(context, embed, widget.readOnly)); return EmbedProxy(widget.embedBuilder(
context, widget.controller, embed, widget.readOnly));
} }
final textSpan = _getTextSpanForWholeLine(context); final textSpan = _getTextSpanForWholeLine(context);
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
@ -173,8 +174,8 @@ class _TextLineState extends State<TextLine> {
} }
// Here it should be image // Here it should be image
final embed = WidgetSpan( final embed = WidgetSpan(
child: EmbedProxy( child: EmbedProxy(widget.embedBuilder(
widget.embedBuilder(context, child, widget.readOnly))); context, widget.controller, child, widget.readOnly)));
textSpanChildren.add(embed); textSpanChildren.add(embed);
continue; continue;
} }

Loading…
Cancel
Save