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();
Widget defaultEmbedBuilderWeb(BuildContext context, Embed node, bool readOnly) {
Widget defaultEmbedBuilderWeb(BuildContext context, QuillController controller,
Embed node, bool readOnly) {
switch (node.value.type) {
case 'image':
final imageUrl = node.value.data;

@ -1,11 +1,13 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/documents/style.dart';
import '../models/quill_delta.dart';
import '../utils/delta.dart';
@ -327,6 +329,21 @@ class QuillController extends ChangeNotifier {
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
Map<String, Attribute> toolbarButtonToggler = {};
}

@ -5,8 +5,8 @@ import 'package:flutter/material.dart';
import '../../flutter_quill.dart';
import 'text_selection.dart';
typedef EmbedBuilder = Widget Function(
BuildContext context, Embed node, bool readOnly);
typedef EmbedBuilder = Widget Function(BuildContext context,
QuillController controller, Embed node, bool readOnly);
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/material.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 '../../utils/platform.dart';
import '../../utils/string.dart';
import '../controller.dart';
import 'image.dart';
import 'image_resizer.dart';
import 'video_app.dart';
import 'youtube_video_app.dart';
Widget defaultEmbedBuilder(
BuildContext context, leaf.Embed node, bool readOnly) {
Widget defaultEmbedBuilder(BuildContext context, QuillController controller,
leaf.Embed node, bool readOnly) {
assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
switch (node.value.type) {
@ -40,34 +45,68 @@ Widget defaultEmbedBuilder(
image ??= imageByUrl(imageUrl);
if (!readOnly && isMobile()) {
// TODO: slider for width and height
// return GestureDetector(
// onTap: () {
// showDialog(
// context: context,
// builder: (context) => Padding(
// padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
// child: SimpleDialog(
// shape: const RoundedRectangleBorder(
// borderRadius:
// BorderRadius.all(Radius.circular(10))),
// children: [
// _SimpleDialogItem(
// icon: Icons.settings_outlined,
// color: Colors.lightBlueAccent,
// text: 'Resize'.i18n,
// onPressed: () {},
// ),
// _SimpleDialogItem(
// icon: Icons.delete_forever_outlined,
// color: Colors.red.shade200,
// text: 'Remove'.i18n,
// onPressed: () {},
// )
// ]),
// ));
// },
// child: image);
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
final resizeOption = _SimpleDialogItem(
icon: Icons.settings_outlined,
color: Colors.lightBlueAccent,
text: 'Resize'.i18n,
onPressed: () {
Navigator.pop(context);
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
return const ImageResizer();
});
},
);
final copyOption = _SimpleDialogItem(
icon: Icons.copy_all_outlined,
color: Colors.cyanAccent,
text: 'Copy'.i18n,
onPressed: () {
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) {
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)) {

@ -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/document.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart';
import '../models/documents/style.dart';
@ -880,6 +881,7 @@ class RawEditorState extends EditorState
@override
void copySelection(SelectionChangedCause cause) {
widget.controller.copiedImageUrl = null;
_pastePlainText = widget.controller.getPlainText();
_pasteStyle = widget.controller.getAllIndividualSelectionStyles();
// Copied straight from EditableTextState
@ -904,8 +906,10 @@ class RawEditorState extends EditorState
@override
void cutSelection(SelectionChangedCause cause) {
widget.controller.copiedImageUrl = null;
_pastePlainText = widget.controller.getPlainText();
_pasteStyle = widget.controller.getAllIndividualSelectionStyles();
// Copied straight from EditableTextState
super.cutSelection(cause);
if (cause == SelectionChangedCause.toolbar) {
@ -916,8 +920,19 @@ class RawEditorState extends EditorState
@override
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
super.pasteText(cause); // ignore: unawaited_futures
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
hideToolbar();

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

Loading…
Cancel
Save