Skip to content

Commit

Permalink
feat(clipboard): allow pasting markdown and html files from the syste…
Browse files Browse the repository at this point in the history
…m to the editor (#1915)

* feat: allow pasting markdown and HTML files from the system into the editor

* chore: format _provideFileAsBytes function parameters

* fix: use onError callback of super_clipboard to work with async await using dart Completer
  • Loading branch information
EchoEllet authored Jun 13, 2024
1 parent 743c829 commit 33ba965
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async' show Completer;
import 'dart:convert' show utf8;

import 'package:flutter/foundation.dart';
// ignore: implementation_imports
Expand All @@ -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) {
Expand All @@ -22,42 +34,100 @@ 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>();

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<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.
Expand All @@ -84,26 +154,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);
Expand Down
48 changes: 20 additions & 28 deletions lib/src/models/documents/delta_x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
25 changes: 25 additions & 0 deletions lib/src/services/clipboard/clipboard_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
64 changes: 50 additions & 14 deletions lib/src/services/clipboard/default_clipboard_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading

0 comments on commit 33ba965

Please sign in to comment.