mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: improve copy paste plugins, and support in-app copy-paste (#3233)
This commit is contained in:
parent
30155924a9
commit
bd30e31f6c
BIN
frontend/appflowy_flutter/assets/test/images/sample.gif
Normal file
BIN
frontend/appflowy_flutter/assets/test/images/sample.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
BIN
frontend/appflowy_flutter/assets/test/images/sample.jpeg
Normal file
BIN
frontend/appflowy_flutter/assets/test/images/sample.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
frontend/appflowy_flutter/assets/test/images/sample.png
Normal file
BIN
frontend/appflowy_flutter/assets/test/images/sample.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
@ -1,5 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -12,36 +14,220 @@ void main() {
|
|||||||
|
|
||||||
group('copy and paste in document', () {
|
group('copy and paste in document', () {
|
||||||
testWidgets('paste multiple lines at the first line', (tester) async {
|
testWidgets('paste multiple lines at the first line', (tester) async {
|
||||||
await tester.initializeAppFlowy();
|
|
||||||
await tester.tapGoButton();
|
|
||||||
|
|
||||||
// create a new document
|
|
||||||
await tester.createNewPageWithName();
|
|
||||||
|
|
||||||
// mock the clipboard
|
// mock the clipboard
|
||||||
const lines = 3;
|
const lines = 3;
|
||||||
AppFlowyClipboard.mockSetData(
|
await tester.pasteContent(
|
||||||
AppFlowyClipboardData(
|
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
|
||||||
text: List.generate(lines, (index) => 'line $index').join('\n'),
|
(editorState) {
|
||||||
),
|
expect(editorState.document.root.children.length, 3);
|
||||||
|
for (var i = 0; i < lines; i++) {
|
||||||
|
expect(
|
||||||
|
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
||||||
|
'line $i',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// paste the text
|
// ## **User Installation**
|
||||||
await tester.simulateKeyEvent(
|
// - [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)
|
||||||
LogicalKeyboardKey.keyV,
|
// - [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)
|
||||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
// - [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)
|
||||||
isMetaPressed: Platform.isMacOS,
|
testWidgets('paste content from html, sample 1', (tester) async {
|
||||||
|
await tester.pasteContent(
|
||||||
|
html:
|
||||||
|
'''<meta charset='utf-8'><h2><strong>User Installation</strong></h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages">Windows/Mac/Linux</a></li>
|
||||||
|
<li><a href="https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker">Docker</a></li>
|
||||||
|
<li><a href="https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source">Source</a></li>
|
||||||
|
</ul>''',
|
||||||
|
(editorState) {
|
||||||
|
expect(editorState.document.root.children.length, 4);
|
||||||
|
final node1 = editorState.getNodeAtPath([0])!;
|
||||||
|
final node2 = editorState.getNodeAtPath([1])!;
|
||||||
|
final node3 = editorState.getNodeAtPath([2])!;
|
||||||
|
final node4 = editorState.getNodeAtPath([3])!;
|
||||||
|
expect(node1.delta!.toJson(), [
|
||||||
|
{
|
||||||
|
"insert": "User Installation",
|
||||||
|
"attributes": {"bold": true},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(node2.delta!.toJson(), [
|
||||||
|
{
|
||||||
|
"insert": "Windows/Mac/Linux",
|
||||||
|
"attributes": {
|
||||||
|
"href":
|
||||||
|
"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
node3.delta!.toJson(),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"insert": "Docker",
|
||||||
|
"attributes": {
|
||||||
|
"href":
|
||||||
|
"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
node4.delta!.toJson(),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"insert": "Source",
|
||||||
|
"attributes": {
|
||||||
|
"href":
|
||||||
|
"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
});
|
||||||
|
|
||||||
final editorState = tester.editor.getCurrentEditorState();
|
testWidgets('paste code from VSCode', (tester) async {
|
||||||
expect(editorState.document.root.children.length, 4);
|
await tester.pasteContent(
|
||||||
for (var i = 0; i < lines; i++) {
|
html:
|
||||||
expect(
|
'''<meta charset='utf-8'><div style="color: #bbbbbb;background-color: #262335;font-family: Consolas, 'JetBrains Mono', monospace, 'cascadia code', Menlo, Monaco, 'Courier New', monospace;font-weight: normal;font-size: 14px;line-height: 21px;white-space: pre;"><div><span style="color: #fede5d;">void</span><span style="color: #ff7edb;"> </span><span style="color: #36f9f6;">main</span><span style="color: #ff7edb;">() {</span></div><div><span style="color: #ff7edb;"> </span><span style="color: #36f9f6;">runApp</span><span style="color: #ff7edb;">(</span><span style="color: #fede5d;">const</span><span style="color: #ff7edb;"> </span><span style="color: #fe4450;">MyApp</span><span style="color: #ff7edb;">());</span></div><div><span style="color: #ff7edb;">}</span></div></div>''',
|
||||||
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
(editorState) {
|
||||||
'line $i',
|
expect(editorState.document.root.children.length, 3);
|
||||||
);
|
final node1 = editorState.getNodeAtPath([0])!;
|
||||||
}
|
final node2 = editorState.getNodeAtPath([1])!;
|
||||||
|
final node3 = editorState.getNodeAtPath([2])!;
|
||||||
|
expect(node1.type, ParagraphBlockKeys.type);
|
||||||
|
expect(node2.type, ParagraphBlockKeys.type);
|
||||||
|
expect(node3.type, ParagraphBlockKeys.type);
|
||||||
|
expect(node1.delta!.toJson(), [
|
||||||
|
{
|
||||||
|
"insert": "void",
|
||||||
|
"attributes": {"font_color": "0xfffede5d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " ",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "main",
|
||||||
|
"attributes": {"font_color": "0xff36f9f6"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "() {",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(node2.delta!.toJson(), [
|
||||||
|
{
|
||||||
|
"insert": " ",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "runApp",
|
||||||
|
"attributes": {"font_color": "0xff36f9f6"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "(",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "const",
|
||||||
|
"attributes": {"font_color": "0xfffede5d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " ",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "MyApp",
|
||||||
|
"attributes": {"font_color": "0xfffe4450"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "());",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(node3.delta!.toJson(), [
|
||||||
|
{
|
||||||
|
"insert": "}",
|
||||||
|
"attributes": {"font_color": "0xffff7edb"},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('paste image(png) from memory', (tester) async {
|
||||||
|
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||||
|
final bytes = image.buffer.asUint8List();
|
||||||
|
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||||
|
expect(editorState.document.root.children.length, 2);
|
||||||
|
final node = editorState.getNodeAtPath([0])!;
|
||||||
|
expect(node.type, ImageBlockKeys.type);
|
||||||
|
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||||
|
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||||
|
final bytes = image.buffer.asUint8List();
|
||||||
|
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||||
|
expect(editorState.document.root.children.length, 2);
|
||||||
|
final node = editorState.getNodeAtPath([0])!;
|
||||||
|
expect(node.type, ImageBlockKeys.type);
|
||||||
|
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('paste image(gif) from memory', (tester) async {
|
||||||
|
// It's not supported yet.
|
||||||
|
// final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||||
|
// final bytes = image.buffer.asUint8List();
|
||||||
|
// await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||||
|
// expect(editorState.document.root.children.length, 2);
|
||||||
|
// final node = editorState.getNodeAtPath([0])!;
|
||||||
|
// expect(node.type, ImageBlockKeys.type);
|
||||||
|
// expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on WidgetTester {
|
||||||
|
Future<void> pasteContent(
|
||||||
|
void Function(EditorState editorState) test, {
|
||||||
|
String? plainText,
|
||||||
|
String? html,
|
||||||
|
(String, Uint8List?)? image,
|
||||||
|
}) async {
|
||||||
|
await initializeAppFlowy();
|
||||||
|
await tapGoButton();
|
||||||
|
|
||||||
|
// create a new document
|
||||||
|
await createNewPageWithName();
|
||||||
|
|
||||||
|
// mock the clipboard
|
||||||
|
getIt<ClipboardService>().setData(
|
||||||
|
ClipboardServiceData(
|
||||||
|
plainText: plainText,
|
||||||
|
html: html,
|
||||||
|
image: image,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// paste the text
|
||||||
|
await simulateKeyEvent(
|
||||||
|
LogicalKeyboardKey.keyV,
|
||||||
|
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||||
|
isMetaPressed: Platform.isMacOS,
|
||||||
|
);
|
||||||
|
await pumpAndSettle();
|
||||||
|
|
||||||
|
final editorState = editor.getCurrentEditorState();
|
||||||
|
test(editorState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||||
toggleToggleListCommand,
|
toggleToggleListCommand,
|
||||||
...codeBlockCommands,
|
...codeBlockCommands,
|
||||||
|
customCopyCommand,
|
||||||
|
customPasteCommand,
|
||||||
...standardCommandShortcutEvents,
|
...standardCommandShortcutEvents,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:super_clipboard/super_clipboard.dart';
|
||||||
|
|
||||||
|
/// Used for in-app copy and paste without losing the format.
|
||||||
|
///
|
||||||
|
/// It's a Json string representing the copied editor nodes.
|
||||||
|
final inAppJsonFormat = CustomValueFormat<String>(
|
||||||
|
applicationId: 'io.appflowy.InAppJsonType',
|
||||||
|
onDecode: (value, platformType) async {
|
||||||
|
if (value is PlatformDataProvider) {
|
||||||
|
final data = await value.getData(platformType);
|
||||||
|
if (data is List<int>) {
|
||||||
|
return utf8.decode(data, allowMalformed: true);
|
||||||
|
}
|
||||||
|
if (data is String) {
|
||||||
|
return Uri.decodeFull(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class ClipboardServiceData {
|
||||||
|
const ClipboardServiceData({
|
||||||
|
this.plainText,
|
||||||
|
this.html,
|
||||||
|
this.image,
|
||||||
|
this.inAppJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? plainText;
|
||||||
|
final String? html;
|
||||||
|
final (String, Uint8List?)? image;
|
||||||
|
final String? inAppJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClipboardService {
|
||||||
|
Future<void> setData(ClipboardServiceData data) async {
|
||||||
|
final plainText = data.plainText;
|
||||||
|
final html = data.html;
|
||||||
|
final inAppJson = data.inAppJson;
|
||||||
|
final image = data.image;
|
||||||
|
|
||||||
|
final item = DataWriterItem();
|
||||||
|
if (plainText != null) {
|
||||||
|
item.add(Formats.plainText(plainText));
|
||||||
|
}
|
||||||
|
if (html != null) {
|
||||||
|
item.add(Formats.htmlText(html));
|
||||||
|
}
|
||||||
|
if (inAppJson != null) {
|
||||||
|
item.add(inAppJsonFormat(inAppJson));
|
||||||
|
}
|
||||||
|
if (image != null && image.$2?.isNotEmpty == true) {
|
||||||
|
switch (image.$1) {
|
||||||
|
case 'png':
|
||||||
|
item.add(Formats.png(image.$2!));
|
||||||
|
break;
|
||||||
|
case 'jpeg':
|
||||||
|
item.add(Formats.jpeg(image.$2!));
|
||||||
|
break;
|
||||||
|
case 'gif':
|
||||||
|
item.add(Formats.gif(image.$2!));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception('unsupported image format: ${image.$1}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ClipboardWriter.instance.write([item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ClipboardServiceData> getData() async {
|
||||||
|
final reader = await ClipboardReader.readClipboard();
|
||||||
|
|
||||||
|
for (final item in reader.items) {
|
||||||
|
final availableFormats = await item.rawReader!.getAvailableFormats();
|
||||||
|
Log.debug(
|
||||||
|
'availableFormats: $availableFormats',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final plainText = await reader.readValue(Formats.plainText);
|
||||||
|
final html = await reader.readValue(Formats.htmlText);
|
||||||
|
final inAppJson = await reader.readValue(inAppJsonFormat);
|
||||||
|
(String, Uint8List?)? image;
|
||||||
|
if (reader.canProvide(Formats.png)) {
|
||||||
|
image = ('png', await reader.readFile(Formats.png));
|
||||||
|
} else if (reader.canProvide(Formats.jpeg)) {
|
||||||
|
image = ('jpeg', await reader.readFile(Formats.jpeg));
|
||||||
|
} else if (reader.canProvide(Formats.gif)) {
|
||||||
|
image = ('gif', await reader.readFile(Formats.gif));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipboardServiceData(
|
||||||
|
plainText: plainText,
|
||||||
|
html: html,
|
||||||
|
image: image,
|
||||||
|
inAppJson: inAppJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on DataReader {
|
||||||
|
Future<Uint8List?>? readFile(FileFormat format) {
|
||||||
|
final c = Completer<Uint8List?>();
|
||||||
|
final progress = getFile(
|
||||||
|
format,
|
||||||
|
(file) async {
|
||||||
|
try {
|
||||||
|
final all = await file.readAll();
|
||||||
|
c.complete(all);
|
||||||
|
} catch (e) {
|
||||||
|
c.completeError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
c.completeError(e);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (progress == null) {
|
||||||
|
c.complete(null);
|
||||||
|
}
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Copy.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
/// - mobile
|
||||||
|
///
|
||||||
|
final CommandShortcutEvent customCopyCommand = CommandShortcutEvent(
|
||||||
|
key: 'copy the selected content',
|
||||||
|
command: 'ctrl+c',
|
||||||
|
macOSCommand: 'cmd+c',
|
||||||
|
handler: _copyCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _copyCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection?.normalized;
|
||||||
|
if (selection == null || selection.isCollapsed) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// plain text.
|
||||||
|
final text = editorState.getTextInSelection(selection).join('\n');
|
||||||
|
|
||||||
|
final nodes = editorState.getSelectedNodes(selection);
|
||||||
|
final document = Document.blank()..insert([0], nodes);
|
||||||
|
|
||||||
|
// in app json
|
||||||
|
final inAppJson = jsonEncode(document.toJson());
|
||||||
|
|
||||||
|
// html
|
||||||
|
final html = documentToHTML(document);
|
||||||
|
|
||||||
|
() async {
|
||||||
|
await getIt<ClipboardService>().setData(
|
||||||
|
ClipboardServiceData(
|
||||||
|
plainText: text,
|
||||||
|
html: html,
|
||||||
|
inAppJson: inAppJson,
|
||||||
|
image: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Paste.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
/// - mobile
|
||||||
|
///
|
||||||
|
final CommandShortcutEvent customPasteCommand = CommandShortcutEvent(
|
||||||
|
key: 'paste the content',
|
||||||
|
command: 'ctrl+v',
|
||||||
|
macOSCommand: 'cmd+v',
|
||||||
|
handler: _pasteCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// because the event handler is not async, so we need to use wrap the async function here
|
||||||
|
() async {
|
||||||
|
// dispatch the paste event
|
||||||
|
final data = await getIt<ClipboardService>().getData();
|
||||||
|
final inAppJson = data.inAppJson;
|
||||||
|
final html = data.html;
|
||||||
|
final plainText = data.plainText;
|
||||||
|
final image = data.image;
|
||||||
|
|
||||||
|
// Order:
|
||||||
|
// 1. in app json format
|
||||||
|
// 2. html
|
||||||
|
// 3. image
|
||||||
|
// 4. plain text
|
||||||
|
|
||||||
|
if (inAppJson != null && inAppJson.isNotEmpty) {
|
||||||
|
await editorState.deleteSelectionIfNeeded();
|
||||||
|
await editorState.pasteInAppJson(inAppJson);
|
||||||
|
} else if (html != null && html.isNotEmpty) {
|
||||||
|
await editorState.deleteSelectionIfNeeded();
|
||||||
|
await editorState.pasteHtml(html);
|
||||||
|
} else if (image != null && image.$2?.isNotEmpty == true) {
|
||||||
|
await editorState.deleteSelectionIfNeeded();
|
||||||
|
await editorState.pasteImage(image.$1, image.$2!);
|
||||||
|
} else if (plainText != null && plainText.isNotEmpty) {
|
||||||
|
await editorState.deleteSelectionIfNeeded();
|
||||||
|
await editorState.pastePlainText(plainText);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
@ -0,0 +1,155 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
extension PasteNodes on EditorState {
|
||||||
|
Future<void> pasteSingleLineNode(Node insertedNode) async {
|
||||||
|
final selection = await deleteSelectionIfNeeded();
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final node = getNodeAtPath(selection.start.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final transaction = this.transaction;
|
||||||
|
final insertedDelta = insertedNode.delta;
|
||||||
|
// if the node is empty, replace it with the inserted node.
|
||||||
|
if (delta.isEmpty || insertedDelta == null) {
|
||||||
|
transaction.insertNode(
|
||||||
|
selection.end.path.next,
|
||||||
|
node.copyWith(
|
||||||
|
type: node.type,
|
||||||
|
attributes: {
|
||||||
|
...node.attributes,
|
||||||
|
...insertedNode.attributes,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
transaction.deleteNode(node);
|
||||||
|
transaction.afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: selection.end.path,
|
||||||
|
offset: insertedDelta?.length ?? 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if the node is not empty, insert the delta from inserted node after the selection.
|
||||||
|
transaction.insertTextDelta(node, selection.endIndex, insertedDelta);
|
||||||
|
}
|
||||||
|
await apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pasteMultiLineNodes(List<Node> nodes) async {
|
||||||
|
assert(nodes.length > 1);
|
||||||
|
|
||||||
|
final selection = await deleteSelectionIfNeeded();
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final node = getNodeAtPath(selection.start.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final transaction = this.transaction;
|
||||||
|
|
||||||
|
final lastNodeLength = nodes.last.delta?.length ?? 0;
|
||||||
|
// merge the current selected node delta into the nodes.
|
||||||
|
if (delta.isNotEmpty) {
|
||||||
|
nodes.first.insertDelta(
|
||||||
|
delta.slice(0, selection.startIndex),
|
||||||
|
insertAfter: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
nodes.last.insertDelta(
|
||||||
|
delta.slice(selection.endIndex),
|
||||||
|
insertAfter: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.isEmpty && node.type != ParagraphBlockKeys.type) {
|
||||||
|
nodes[0] = nodes.first.copyWith(
|
||||||
|
type: node.type,
|
||||||
|
attributes: {
|
||||||
|
...node.attributes,
|
||||||
|
...nodes.first.attributes,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final child in node.children) {
|
||||||
|
nodes.last.insert(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.insertNodes(selection.end.path, nodes);
|
||||||
|
|
||||||
|
// delete the current node.
|
||||||
|
transaction.deleteNode(node);
|
||||||
|
|
||||||
|
var path = selection.end.path;
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
path = path.next;
|
||||||
|
}
|
||||||
|
transaction.afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: path.previous, // because a node is deleted.
|
||||||
|
offset: lastNodeLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection if it's not collapsed.
|
||||||
|
Future<Selection?> deleteSelectionIfNeeded() async {
|
||||||
|
final selection = this.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection first.
|
||||||
|
if (!selection.isCollapsed) {
|
||||||
|
deleteSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch selection again.selection = editorState.selection;
|
||||||
|
assert(this.selection?.isCollapsed == true);
|
||||||
|
return this.selection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Node {
|
||||||
|
void insertDelta(Delta delta, {bool insertAfter = true}) {
|
||||||
|
assert(delta.every((element) => element is TextInsert));
|
||||||
|
if (this.delta == null) {
|
||||||
|
updateAttributes({
|
||||||
|
blockComponentDelta: delta.toJson(),
|
||||||
|
});
|
||||||
|
} else if (insertAfter) {
|
||||||
|
updateAttributes(
|
||||||
|
{
|
||||||
|
blockComponentDelta: this
|
||||||
|
.delta!
|
||||||
|
.compose(
|
||||||
|
Delta()
|
||||||
|
..retain(this.delta!.length)
|
||||||
|
..addAll(delta),
|
||||||
|
)
|
||||||
|
.toJson(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateAttributes(
|
||||||
|
{
|
||||||
|
blockComponentDelta: delta
|
||||||
|
.compose(
|
||||||
|
Delta()
|
||||||
|
..retain(delta.length)
|
||||||
|
..addAll(this.delta!),
|
||||||
|
)
|
||||||
|
.toJson(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
extension PasteFromHtml on EditorState {
|
||||||
|
Future<void> pasteHtml(String html) async {
|
||||||
|
final nodes = htmlToDocument(html).root.children.toList();
|
||||||
|
// remove the front and back empty line
|
||||||
|
while (nodes.isNotEmpty && nodes.first.delta?.isEmpty == true) {
|
||||||
|
nodes.removeAt(0);
|
||||||
|
}
|
||||||
|
while (nodes.isNotEmpty && nodes.last.delta?.isEmpty == true) {
|
||||||
|
nodes.removeLast();
|
||||||
|
}
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodes.length == 1) {
|
||||||
|
await pasteSingleLineNode(nodes.first);
|
||||||
|
} else {
|
||||||
|
await pasteMultiLineNodes(nodes.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
extension PasteFromImage on EditorState {
|
||||||
|
static final supportedImageFormats = [
|
||||||
|
'png',
|
||||||
|
'jpeg',
|
||||||
|
'gif',
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> pasteImage(String format, Uint8List imageBytes) async {
|
||||||
|
if (!supportedImageFormats.contains(format)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||||
|
final imagePath = p.join(
|
||||||
|
path,
|
||||||
|
'images',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// create the directory if not exists
|
||||||
|
final directory = Directory(imagePath);
|
||||||
|
if (!directory.existsSync()) {
|
||||||
|
await directory.create(recursive: true);
|
||||||
|
}
|
||||||
|
final copyToPath = p.join(
|
||||||
|
imagePath,
|
||||||
|
'${uuid()}.$format',
|
||||||
|
);
|
||||||
|
await File(copyToPath).writeAsBytes(imageBytes);
|
||||||
|
await insertImageNode(copyToPath);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('cannot copy image file', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
|
|
||||||
|
extension PasteFromInAppJson on EditorState {
|
||||||
|
Future<void> pasteInAppJson(String inAppJson) async {
|
||||||
|
try {
|
||||||
|
final nodes = Document.fromJson(jsonDecode(inAppJson)).root.children;
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodes.length == 1) {
|
||||||
|
await pasteSingleLineNode(nodes.first);
|
||||||
|
} else {
|
||||||
|
await pasteMultiLineNodes(nodes.toList());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
|
'Failed to paste in app json: $inAppJson, error: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
RegExp _hrefRegex = RegExp(
|
||||||
|
r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?',
|
||||||
|
);
|
||||||
|
|
||||||
|
extension PasteFromPlainText on EditorState {
|
||||||
|
Future<void> pastePlainText(String plainText) async {
|
||||||
|
final nodes = plainText
|
||||||
|
.split('\n')
|
||||||
|
.map(
|
||||||
|
(e) => e
|
||||||
|
..replaceAll(r'\r', '')
|
||||||
|
..trimRight(),
|
||||||
|
)
|
||||||
|
.map((e) {
|
||||||
|
// parse the url content
|
||||||
|
final Attributes attributes = {};
|
||||||
|
if (_hrefRegex.hasMatch(e)) {
|
||||||
|
attributes[AppFlowyRichTextKeys.href] = e;
|
||||||
|
}
|
||||||
|
return Delta()..insert(e, attributes: attributes);
|
||||||
|
})
|
||||||
|
.map((e) => paragraphNode(delta: e))
|
||||||
|
.toList();
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodes.length == 1) {
|
||||||
|
await pasteSingleLineNode(nodes.first);
|
||||||
|
} else {
|
||||||
|
await pasteMultiLineNodes(nodes.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,8 @@ export 'actions/option_action.dart';
|
|||||||
export 'callout/callout_block_component.dart';
|
export 'callout/callout_block_component.dart';
|
||||||
export 'code_block/code_block_component.dart';
|
export 'code_block/code_block_component.dart';
|
||||||
export 'code_block/code_block_shortcut_event.dart';
|
export 'code_block/code_block_shortcut_event.dart';
|
||||||
|
export 'copy_and_paste/custom_copy_command.dart';
|
||||||
|
export 'copy_and_paste/custom_paste_command.dart';
|
||||||
export 'database/database_view_block_component.dart';
|
export 'database/database_view_block_component.dart';
|
||||||
export 'database/inline_database_menu_item.dart';
|
export 'database/inline_database_menu_item.dart';
|
||||||
export 'database/referenced_database_menu_item.dart';
|
export 'database/referenced_database_menu_item.dart';
|
||||||
|
@ -6,28 +6,29 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
|
|||||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
|
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
|
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||||
|
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
||||||
|
import 'package:appflowy/user/application/prelude.dart';
|
||||||
import 'package:appflowy/user/application/user_listener.dart';
|
import 'package:appflowy/user/application/user_listener.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|
||||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
|
||||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
|
||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
|
||||||
import 'package:appflowy/workspace/application/workspace/prelude.dart';
|
|
||||||
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
|
||||||
import 'package:appflowy/user/application/prelude.dart';
|
|
||||||
import 'package:appflowy/user/presentation/router.dart';
|
import 'package:appflowy/user/presentation/router.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/workspace/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
|
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||||
|
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@ -79,6 +80,10 @@ void _resolveCommonService(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt.registerFactory<ClipboardService>(
|
||||||
|
() => ClipboardService(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveUserDeps(GetIt getIt) {
|
void _resolveUserDeps(GetIt getIt) {
|
||||||
|
@ -318,10 +318,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "499c61743e13909c13374a8c209075385858c614b9c0f2487b5f9995eeaf7369"
|
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.1"
|
version: "9.0.3"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -721,6 +721,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
irondash_engine_context:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: irondash_engine_context
|
||||||
|
sha256: "6431b11844472574a90803c02f1e55221e6a390a872786735f6757a67dacd678"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
|
irondash_message_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: irondash_message_channel
|
||||||
|
sha256: "4114739083d1c63e6a1a8b93f09dd69b3cf9a9d6ee215ae7f23079307197ebba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
isolates:
|
isolates:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1001,6 +1017,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.0"
|
version: "5.4.0"
|
||||||
|
pixel_snap:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pixel_snap
|
||||||
|
sha256: "5de3662b926c9bc189578cf90f9d5b350ee61bc8e20e8a91fa1dfdd26c9f5ece"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1423,6 +1447,22 @@ packages:
|
|||||||
url: "https://github.com/LucasXu0/supabase-flutter"
|
url: "https://github.com/LucasXu0/supabase-flutter"
|
||||||
source: git
|
source: git
|
||||||
version: "1.10.12"
|
version: "1.10.12"
|
||||||
|
super_clipboard:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: super_clipboard
|
||||||
|
sha256: "204284b1a721d33a65bcab077b191a3b7379b46a231f05688d17220153338ede"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
|
super_native_extensions:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: super_native_extensions
|
||||||
|
sha256: "1f15e9b1adb0bc59cf9b889a0b248f3c192fa17e2d5c923aeeec6d4fa2eeffd6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
sync_http:
|
sync_http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -107,6 +107,7 @@ dependencies:
|
|||||||
url_protocol:
|
url_protocol:
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
|
super_clipboard: ^0.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
@ -200,6 +201,7 @@ flutter:
|
|||||||
# The following assets will be excluded in release.
|
# The following assets will be excluded in release.
|
||||||
# BEGIN: EXCLUDE_IN_RELEASE
|
# BEGIN: EXCLUDE_IN_RELEASE
|
||||||
- assets/test/workspaces/
|
- assets/test/workspaces/
|
||||||
|
- assets/test/images/
|
||||||
- assets/template/
|
- assets/template/
|
||||||
- assets/test/workspaces/markdowns/
|
- assets/test/workspaces/markdowns/
|
||||||
- assets/test/workspaces/database/
|
- assets/test/workspaces/database/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user