feat: allow pasting markdown and html files from the system to the editor

pull/1915/head
Ellet 10 months ago
parent 743c829e48
commit f78789efe0
  1. 115
      flutter_quill_extensions/lib/services/clipboard/super_clipboard_service.dart
  2. 48
      lib/src/models/documents/delta_x.dart
  3. 25
      lib/src/services/clipboard/clipboard_service.dart
  4. 64
      lib/src/services/clipboard/default_clipboard_service.dart
  5. 69
      lib/src/widgets/quill/quill_controller.dart

@ -1,4 +1,5 @@
import 'dart:async' show Completer;
import 'dart:convert' show utf8;
import 'package:flutter/foundation.dart';
// ignore: implementation_imports
@ -13,6 +14,17 @@ class SuperClipboardService implements ClipboardService {
return SystemClipboard.instance;
}
SystemClipboard _getSuperClipboardOrThrow() {
final clipboard = _getSuperClipboard();
if (clipboard == null) {
// To avoid getting this exception, use _canProvide()
throw UnsupportedError(
'Clipboard API is not supported on this platform.',
);
}
return clipboard;
}
Future<bool> _canProvide({required DataFormat format}) async {
final clipboard = _getSuperClipboard();
if (clipboard == null) {
@ -22,14 +34,9 @@ class SuperClipboardService implements ClipboardService {
return reader.canProvide(format);
}
Future<Uint8List> _provideFileAsBytes({required FileFormat format}) async {
final clipboard = _getSuperClipboard();
if (clipboard == null) {
// To avoid getting this exception, use _canProvide()
throw UnsupportedError(
'Clipboard API is not supported on this platform.',
);
}
Future<Uint8List> _provideFileAsBytes(
{required SimpleFileFormat format}) async {
final clipboard = _getSuperClipboardOrThrow();
final reader = await clipboard.read();
final completer = Completer<Uint8List>();
@ -41,23 +48,81 @@ class SuperClipboardService implements ClipboardService {
return bytes;
}
Future<String> _provideFileAsString({
required SimpleFileFormat format,
}) async {
final fileBytes = await _provideFileAsBytes(format: format);
final fileText = utf8.decode(fileBytes);
return fileText;
}
/// According to super_clipboard docs, will return `null` if the value
/// is not available or the data is virtual (macOS and Windows)
Future<String?> _provideSimpleValueFormatAsString({
required SimpleValueFormat<String> format,
}) async {
final clipboard = _getSuperClipboard();
if (clipboard == null) {
// To avoid getting this exception, use _canProvide()
throw UnsupportedError(
'Clipboard API is not supported on this platform.',
);
}
final clipboard = _getSuperClipboardOrThrow();
final reader = await clipboard.read();
final value = await reader.readValue<String>(format);
return value;
}
@override
Future<bool> canProvideHtmlText() {
return _canProvide(format: Formats.htmlText);
}
@override
Future<String?> getHtmlText() {
return _provideSimpleValueFormatAsString(format: Formats.htmlText);
}
@override
Future<bool> canProvideHtmlTextFromFile() {
return _canProvide(format: Formats.htmlFile);
}
@override
Future<String?> getHtmlTextFromFile() {
return _provideFileAsString(format: Formats.htmlFile);
}
@override
Future<bool> canProvideMarkdownText() async {
// Formats.markdownText or Formats.mdText does not exist yet in super_clipboard
return false;
}
@override
Future<String?> getMarkdownText() async {
// Formats.markdownText or Formats.mdText does not exist yet in super_clipboard
throw UnsupportedError(
'SuperClipboardService does not support retrieving image files.',
);
}
@override
Future<bool> canProvideMarkdownTextFromFile() async {
// Formats.md is for markdown files
return _canProvide(format: Formats.md);
}
@override
Future<String?> getMarkdownTextFromFile() async {
// Formats.md is for markdown files
return _provideFileAsString(format: Formats.md);
}
@override
Future<bool> canProvidePlainText() {
return _canProvide(format: Formats.plainText);
}
@override
Future<String?> getPlainText() {
return _provideSimpleValueFormatAsString(format: Formats.plainText);
}
/// This will need to be updated if [getImageFileAsBytes] updated.
/// Notice that even if the copied image is JPEG, it still can be provided
/// as PNG, will handle JPEG check in case this info is incorrect.
@ -84,26 +149,6 @@ class SuperClipboardService implements ClipboardService {
return _provideFileAsBytes(format: Formats.jpeg);
}
@override
Future<bool> canProvidePlainText() {
return _canProvide(format: Formats.plainText);
}
@override
Future<String?> getPlainText() {
return _provideSimpleValueFormatAsString(format: Formats.plainText);
}
@override
Future<bool> canProvideHtmlText() {
return _canProvide(format: Formats.htmlText);
}
@override
Future<String?> getHtmlText() {
return _provideSimpleValueFormatAsString(format: Formats.htmlText);
}
@override
Future<bool> canProvideGifFile() {
return _canProvide(format: Formats.gif);

@ -6,43 +6,35 @@ import '../../../markdown_quill.dart';
import '../../../quill_delta.dart';
@immutable
@experimental
class DeltaX {
const DeltaX._();
/// Convert Markdown text to [Delta]
///
/// This api is **experimental** and designed to be used **internally** and shouldn't
/// used for **production applications**.
@experimental
static Delta fromMarkdown(String markdownText) {
final mdDocument = md.Document(encodeHtml: false);
final mdToDelta = MarkdownToDelta(markdownDocument: mdDocument);
return mdToDelta.convert(markdownText);
}
/// Convert the HTML Raw string to [Delta]
///
/// It will run using the following steps:
///
/// 1. Convert the html to markdown string using `html2md` package
/// 2. Convert the markdown string to quill delta json string
/// 3. Decode the delta json string to [Delta]
/// 2. Convert the markdown string to [Delta] using [fromMarkdown]
///
/// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100)
///
/// Please notice that this api is designed to be used internally and shouldn't
/// used for real world applications
/// This api is **experimental** and designed to be used **internally** and shouldn't
/// used for **production applications**.
///
@experimental
static Delta fromHtml(String html) {
final markdown = html2md
.convert(
html,
)
.replaceAll('unsafe:', '');
final mdDocument = md.Document(encodeHtml: false);
final mdToDelta = MarkdownToDelta(markdownDocument: mdDocument);
return mdToDelta.convert(markdown);
static Delta fromHtml(String htmlText) {
final markdownText = html2md.convert(htmlText).replaceAll('unsafe:', '');
// final deltaJsonString = markdownToDelta(markdown);
// final deltaJson = jsonDecode(deltaJsonString);
// if (deltaJson is! List) {
// throw ArgumentError(
// 'The delta json string should be of type list when jsonDecode() it',
// );
// }
// return Delta.fromJson(
// deltaJson,
// );
return fromMarkdown(markdownText);
}
}

@ -4,8 +4,33 @@ import 'package:flutter/foundation.dart';
@immutable
abstract class ClipboardService {
Future<bool> canProvideHtmlText();
/// Get Clipboard content as Html Text, this is platform specific and not the
/// same as [getPlainText] for two reasons:
/// 1. The user might want to paste Html text
/// 2. Copying Html text from other apps and use [getPlainText] will ignore
/// the Html content and provide it as text
Future<String?> getHtmlText();
Future<bool> canProvideHtmlTextFromFile();
/// Get the Html file in the Clipboard from the system
Future<String?> getHtmlTextFromFile();
Future<bool> canProvideMarkdownText();
/// Get Clipboard content as Markdown Text, this is platform specific and not the
/// same as [getPlainText] for two reasons:
/// 1. The user might want to paste Markdown text
/// 2. Copying Markdown text from other apps and use [getPlainText] will ignore
/// the Markdown content and provide it as text
Future<String?> getMarkdownText();
Future<bool> canProvideMarkdownTextFromFile();
/// Get the Markdown file in the Clipboard from the system
Future<String?> getMarkdownTextFromFile();
Future<bool> canProvidePlainText();
Future<String?> getPlainText();

@ -5,40 +5,70 @@ import 'clipboard_service.dart';
/// Default implementation using only internal flutter plugins
class DefaultClipboardService implements ClipboardService {
@override
Future<bool> canProvideGifFile() async {
Future<bool> canProvideHtmlText() async {
return false;
}
@override
Future<bool> canProvideHtmlText() async {
return false;
Future<String?> getHtmlText() {
throw UnsupportedError(
'DefaultClipboardService does not support retrieving HTML text.',
);
}
@override
Future<bool> canProvideImageFile() async {
Future<bool> canProvideHtmlTextFromFile() async {
return false;
}
@override
Future<bool> canProvidePlainText() async {
final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text;
return plainText == null;
Future<String?> getHtmlTextFromFile() {
throw UnsupportedError(
'DefaultClipboardService does not support retrieving HTML files.',
);
}
@override
Future<Uint8List> getGifFileAsBytes() {
Future<bool> canProvideMarkdownText() async {
return false;
}
@override
Future<String?> getMarkdownText() {
throw UnsupportedError(
'DefaultClipboardService does not support retrieving GIF files.',
'DefaultClipboardService does not support retrieving HTML files.',
);
}
@override
Future<String?> getHtmlText() {
Future<bool> canProvideMarkdownTextFromFile() async {
return false;
}
@override
Future<String?> getMarkdownTextFromFile() {
throw UnsupportedError(
'DefaultClipboardService does not support retrieving HTML text.',
'DefaultClipboardService does not support retrieving Markdown text.',
);
}
@override
Future<bool> canProvidePlainText() async {
final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text;
return plainText == null;
}
@override
Future<String?> getPlainText() async {
final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text;
return plainText;
}
@override
Future<bool> canProvideImageFile() async {
return false;
}
@override
Future<Uint8List> getImageFileAsBytes() {
throw UnsupportedError(
@ -47,9 +77,15 @@ class DefaultClipboardService implements ClipboardService {
}
@override
Future<String?> getPlainText() async {
final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text;
return plainText;
Future<bool> canProvideGifFile() async {
return false;
}
@override
Future<Uint8List> getGifFileAsBytes() {
throw UnsupportedError(
'DefaultClipboardService does not support retrieving GIF files.',
);
}
@override

@ -512,6 +512,11 @@ class QuillController extends ChangeNotifier {
return true;
}
if (await _pasteMarkdown()) {
updateEditor?.call();
return true;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final plainTextClipboardData =
@ -550,24 +555,60 @@ class QuillController extends ChangeNotifier {
return false;
}
/// True if can paste using HTML
void _pasteUsingDelta(Delta deltaFromClipboard) {
replaceText(
selection.start,
selection.end - selection.start,
deltaFromClipboard,
TextSelection.collapsed(offset: selection.end),
);
}
/// Return true if can paste using HTML
Future<bool> _pasteHTML() async {
final clipboardService = ClipboardServiceProvider.instacne;
if (await clipboardService.canProvideHtmlText()) {
final html = await clipboardService.getHtmlText();
if (html == null) {
return false;
Future<String?> getHTML() async {
if (await clipboardService.canProvideHtmlTextFromFile()) {
return await clipboardService.getHtmlTextFromFile();
}
final htmlBody = html_parser.parse(html).body?.outerHtml;
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? html);
replaceText(
selection.start,
selection.end - selection.start,
deltaFromClipboard,
TextSelection.collapsed(offset: selection.end),
);
if (await clipboardService.canProvideHtmlText()) {
return await clipboardService.getHtmlText();
}
return null;
}
final htmlText = await getHTML();
if (htmlText != null) {
final htmlBody = html_parser.parse(htmlText).body?.outerHtml;
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText);
_pasteUsingDelta(deltaFromClipboard);
return true;
}
return false;
}
/// Return true if can paste using Markdown
Future<bool> _pasteMarkdown() async {
final clipboardService = ClipboardServiceProvider.instacne;
Future<String?> getMarkdown() async {
if (await clipboardService.canProvideMarkdownTextFromFile()) {
return await clipboardService.getMarkdownTextFromFile();
}
if (await clipboardService.canProvideMarkdownText()) {
return await clipboardService.getMarkdownText();
}
return null;
}
final markdownText = await getMarkdown();
if (markdownText != null) {
final deltaFromClipboard = DeltaX.fromMarkdown(markdownText);
_pasteUsingDelta(deltaFromClipboard);
return true;
}

Loading…
Cancel
Save