diff --git a/flutter_quill_extensions/lib/services/clipboard/super_clipboard_service.dart b/flutter_quill_extensions/lib/services/clipboard/super_clipboard_service.dart index a808c9b5..2514f7a8 100644 --- a/flutter_quill_extensions/lib/services/clipboard/super_clipboard_service.dart +++ b/flutter_quill_extensions/lib/services/clipboard/super_clipboard_service.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 _canProvide({required DataFormat format}) async { final clipboard = _getSuperClipboard(); if (clipboard == null) { @@ -22,42 +34,100 @@ class SuperClipboardService implements ClipboardService { return reader.canProvide(format); } - Future _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 _provideFileAsBytes({ + required SimpleFileFormat format, + }) async { + final clipboard = _getSuperClipboardOrThrow(); final reader = await clipboard.read(); final completer = Completer(); - reader.getFile(format, (file) async { - final bytes = await file.readAll(); - completer.complete(bytes); - }); + reader.getFile( + format, + (file) async { + final bytes = await file.readAll(); + completer.complete(bytes); + }, + onError: completer.completeError, + ); final bytes = await completer.future; return bytes; } + Future _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 _provideSimpleValueFormatAsString({ required SimpleValueFormat 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(format); return value; } + @override + Future canProvideHtmlText() { + return _canProvide(format: Formats.htmlText); + } + + @override + Future getHtmlText() { + return _provideSimpleValueFormatAsString(format: Formats.htmlText); + } + + @override + Future canProvideHtmlTextFromFile() { + return _canProvide(format: Formats.htmlFile); + } + + @override + Future getHtmlTextFromFile() { + return _provideFileAsString(format: Formats.htmlFile); + } + + @override + Future canProvideMarkdownText() async { + // Formats.markdownText or Formats.mdText does not exist yet in super_clipboard + return false; + } + + @override + Future 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 canProvideMarkdownTextFromFile() async { + // Formats.md is for markdown files + return _canProvide(format: Formats.md); + } + + @override + Future getMarkdownTextFromFile() async { + // Formats.md is for markdown files + return _provideFileAsString(format: Formats.md); + } + + @override + Future canProvidePlainText() { + return _canProvide(format: Formats.plainText); + } + + @override + Future 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 +154,6 @@ class SuperClipboardService implements ClipboardService { return _provideFileAsBytes(format: Formats.jpeg); } - @override - Future canProvidePlainText() { - return _canProvide(format: Formats.plainText); - } - - @override - Future getPlainText() { - return _provideSimpleValueFormatAsString(format: Formats.plainText); - } - - @override - Future canProvideHtmlText() { - return _canProvide(format: Formats.htmlText); - } - - @override - Future getHtmlText() { - return _provideSimpleValueFormatAsString(format: Formats.htmlText); - } - @override Future canProvideGifFile() { return _canProvide(format: Formats.gif); diff --git a/lib/src/models/documents/delta_x.dart b/lib/src/models/documents/delta_x.dart index 76c6489d..0762fa15 100644 --- a/lib/src/models/documents/delta_x.dart +++ b/lib/src/models/documents/delta_x.dart @@ -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); } } diff --git a/lib/src/services/clipboard/clipboard_service.dart b/lib/src/services/clipboard/clipboard_service.dart index 5043127f..848821f5 100644 --- a/lib/src/services/clipboard/clipboard_service.dart +++ b/lib/src/services/clipboard/clipboard_service.dart @@ -4,8 +4,33 @@ import 'package:flutter/foundation.dart'; @immutable abstract class ClipboardService { Future 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 getHtmlText(); + Future canProvideHtmlTextFromFile(); + + /// Get the Html file in the Clipboard from the system + Future getHtmlTextFromFile(); + + Future 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 getMarkdownText(); + + Future canProvideMarkdownTextFromFile(); + + /// Get the Markdown file in the Clipboard from the system + Future getMarkdownTextFromFile(); + Future canProvidePlainText(); Future getPlainText(); diff --git a/lib/src/services/clipboard/default_clipboard_service.dart b/lib/src/services/clipboard/default_clipboard_service.dart index a65df0d8..4509c110 100644 --- a/lib/src/services/clipboard/default_clipboard_service.dart +++ b/lib/src/services/clipboard/default_clipboard_service.dart @@ -5,40 +5,70 @@ import 'clipboard_service.dart'; /// Default implementation using only internal flutter plugins class DefaultClipboardService implements ClipboardService { @override - Future canProvideGifFile() async { + Future canProvideHtmlText() async { return false; } @override - Future canProvideHtmlText() async { - return false; + Future getHtmlText() { + throw UnsupportedError( + 'DefaultClipboardService does not support retrieving HTML text.', + ); } @override - Future canProvideImageFile() async { + Future canProvideHtmlTextFromFile() async { return false; } @override - Future canProvidePlainText() async { - final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text; - return plainText == null; + Future getHtmlTextFromFile() { + throw UnsupportedError( + 'DefaultClipboardService does not support retrieving HTML files.', + ); } @override - Future getGifFileAsBytes() { + Future canProvideMarkdownText() async { + return false; + } + + @override + Future getMarkdownText() { throw UnsupportedError( - 'DefaultClipboardService does not support retrieving GIF files.', + 'DefaultClipboardService does not support retrieving HTML files.', ); } @override - Future getHtmlText() { + Future canProvideMarkdownTextFromFile() async { + return false; + } + + @override + Future getMarkdownTextFromFile() { throw UnsupportedError( - 'DefaultClipboardService does not support retrieving HTML text.', + 'DefaultClipboardService does not support retrieving Markdown text.', ); } + @override + Future canProvidePlainText() async { + final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text; + return plainText == null; + } + + @override + Future getPlainText() async { + final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text; + return plainText; + } + + @override + Future canProvideImageFile() async { + return false; + } + @override Future getImageFileAsBytes() { throw UnsupportedError( @@ -47,9 +77,15 @@ class DefaultClipboardService implements ClipboardService { } @override - Future getPlainText() async { - final plainText = (await Clipboard.getData(Clipboard.kTextPlain))?.text; - return plainText; + Future canProvideGifFile() async { + return false; + } + + @override + Future getGifFileAsBytes() { + throw UnsupportedError( + 'DefaultClipboardService does not support retrieving GIF files.', + ); } @override diff --git a/lib/src/widgets/quill/quill_controller.dart b/lib/src/widgets/quill/quill_controller.dart index f9c18fdb..012f1a25 100644 --- a/lib/src/widgets/quill/quill_controller.dart +++ b/lib/src/widgets/quill/quill_controller.dart @@ -507,7 +507,14 @@ class QuillController extends ChangeNotifier { Future clipboardPaste({void Function()? updateEditor}) async { if (readOnly || !selection.isValid) return true; - if (await _pasteHTML()) { + final pasteUsingHtmlSuccess = await _pasteHTML(); + if (pasteUsingHtmlSuccess) { + updateEditor?.call(); + return true; + } + + final pasteUsingMarkdownSuccess = await _pasteMarkdown(); + if (pasteUsingMarkdownSuccess) { updateEditor?.call(); return true; } @@ -550,24 +557,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 _pasteHTML() async { final clipboardService = ClipboardServiceProvider.instacne; - if (await clipboardService.canProvideHtmlText()) { - final html = await clipboardService.getHtmlText(); - if (html == null) { - return false; + Future 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 _pasteMarkdown() async { + final clipboardService = ClipboardServiceProvider.instacne; + + Future 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; }