mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: document migration from 0.1.x to 0.2.0 (#2583)
* chore: migrate the rewrite feature * chore: rename flowy-document * feat: add initial_data interface * chore: rename the document event * fix: font name error * fix: export page UI issues * feat: implement editor migration 0.1.x -> 0.2.0 * feat: support import old json * fix: nested list error * chore: update pubspec
This commit is contained in:
parent
f8d09e4894
commit
ffff628359
254
frontend/appflowy_flutter/assets/template/readme.json
Normal file
254
frontend/appflowy_flutter/assets/template/readme.json
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
{
|
||||||
|
"document": {
|
||||||
|
"type": "editor",
|
||||||
|
"children": [
|
||||||
|
{ "type": "cover" },
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "heading", "heading": "h1" },
|
||||||
|
"delta": [{ "insert": "Welcome to AppFlowy!" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "heading", "heading": "h2" },
|
||||||
|
"delta": [{ "insert": "Here are the basics" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||||
|
"delta": [{ "insert": "Click anywhere and just start typing." }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": false },
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Highlight ",
|
||||||
|
"attributes": { "backgroundColor": "0x4dffeb3b" }
|
||||||
|
},
|
||||||
|
{ "insert": "any text, and use the editing menu to " },
|
||||||
|
{ "insert": "style", "attributes": { "italic": true } },
|
||||||
|
{ "insert": " " },
|
||||||
|
{ "insert": "your", "attributes": { "bold": true } },
|
||||||
|
{ "insert": " " },
|
||||||
|
{ "insert": "writing", "attributes": { "underline": true } },
|
||||||
|
{ "insert": " " },
|
||||||
|
{ "insert": "however", "attributes": { "code": true } },
|
||||||
|
{ "insert": " you " },
|
||||||
|
{ "insert": "like.", "attributes": { "strikethrough": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "As soon as you type " },
|
||||||
|
{
|
||||||
|
"insert": "/",
|
||||||
|
"attributes": { "code": true, "color": "0xff00b5ff" }
|
||||||
|
},
|
||||||
|
{ "insert": " a menu will pop up. Select " },
|
||||||
|
{
|
||||||
|
"insert": "different types",
|
||||||
|
"attributes": { "backgroundColor": "0x4d9c27b0" }
|
||||||
|
},
|
||||||
|
{ "insert": " of content blocks you can add." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Type " },
|
||||||
|
{ "insert": "/", "attributes": { "code": true } },
|
||||||
|
{ "insert": " followed by " },
|
||||||
|
{ "insert": "/bullet", "attributes": { "code": true } },
|
||||||
|
{ "insert": " or " },
|
||||||
|
{ "insert": "/num", "attributes": { "code": true } },
|
||||||
|
{ "insert": " to create a list.", "attributes": { "code": false } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": true },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Click " },
|
||||||
|
{ "insert": "+ New Page ", "attributes": { "code": true } },
|
||||||
|
{
|
||||||
|
"insert": "button at the bottom of your sidebar to add a new page."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Click " },
|
||||||
|
{ "insert": "+", "attributes": { "code": true } },
|
||||||
|
{ "insert": " next to any page title in the sidebar to " },
|
||||||
|
{ "insert": "quickly", "attributes": { "color": "0xff8427e0" } },
|
||||||
|
{ "insert": " add a new subpage, " },
|
||||||
|
{ "insert": "Document", "attributes": { "code": true } },
|
||||||
|
{ "insert": ", ", "attributes": { "code": false } },
|
||||||
|
{ "insert": "Grid", "attributes": { "code": true } },
|
||||||
|
{ "insert": ", or ", "attributes": { "code": false } },
|
||||||
|
{ "insert": "Kanban Board", "attributes": { "code": true } },
|
||||||
|
{ "insert": ".", "attributes": { "code": false } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "text", "delta": [] },
|
||||||
|
{ "type": "divider" },
|
||||||
|
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"checkbox": null,
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 1,
|
||||||
|
"heading": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Keyboard shortcuts " },
|
||||||
|
{
|
||||||
|
"insert": "guide",
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 2,
|
||||||
|
"heading": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Markdown " },
|
||||||
|
{
|
||||||
|
"insert": "reference",
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "number": 3, "subtype": "number-list" },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Type " },
|
||||||
|
{ "insert": "/code", "attributes": { "code": true } },
|
||||||
|
{
|
||||||
|
"insert": " to insert a code block",
|
||||||
|
"attributes": { "code": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "code_block",
|
||||||
|
"number": 3,
|
||||||
|
"heading": null,
|
||||||
|
"number-list": null,
|
||||||
|
"theme": "vs",
|
||||||
|
"language": "rust"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||||
|
},
|
||||||
|
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"checkbox": null,
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [{ "insert": "Have a question❓" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "quote" },
|
||||||
|
"delta": [
|
||||||
|
{ "insert": "Click " },
|
||||||
|
{ "insert": "?", "attributes": { "code": true } },
|
||||||
|
{ "insert": " at the bottom right for help and support." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "text", "delta": [] },
|
||||||
|
{
|
||||||
|
"type": "callout",
|
||||||
|
"children": [
|
||||||
|
{ "type": "text", "delta": [] },
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "heading", "heading": "h2" },
|
||||||
|
"delta": [{ "insert": "Like AppFlowy? Follow us:" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "bulleted-list" },
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "GitHub",
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "bulleted-list" },
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Twitter",
|
||||||
|
"attributes": { "href": "https://twitter.com/appflowy" }
|
||||||
|
},
|
||||||
|
{ "insert": ": @appflowy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": "bulleted-list" },
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Newsletter",
|
||||||
|
"attributes": { "href": "https://blog-appflowy.ghost.io/" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": { "emoji": "😀" }
|
||||||
|
},
|
||||||
|
{ "type": "text", "delta": [] },
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": null, "heading": null },
|
||||||
|
"delta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": { "subtype": null, "heading": null },
|
||||||
|
"delta": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy/util/json_print.dart';
|
import 'package:appflowy/util/json_print.dart';
|
||||||
@ -14,6 +15,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
@ -129,7 +131,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAppFlowyEditorState(DocumentDataPB2 data) async {
|
Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
prettyPrintJson(data.toProto3Json());
|
prettyPrintJson(data.toProto3Json());
|
||||||
}
|
}
|
||||||
@ -140,7 +142,11 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final editorState = EditorState(document: document);
|
// test: read from asset
|
||||||
|
final readme = await rootBundle.loadString('assets/template/readme.json');
|
||||||
|
final readmeDocument = EditorMigration.migrateDocument(readme);
|
||||||
|
|
||||||
|
final editorState = EditorState(document: readmeDocument);
|
||||||
this.editorState = editorState;
|
this.editorState = editorState;
|
||||||
|
|
||||||
// subscribe to the document change from the editor
|
// subscribe to the document change from the editor
|
||||||
@ -189,6 +195,6 @@ class DocumentState with _$DocumentState {
|
|||||||
class DocumentLoadingState with _$DocumentLoadingState {
|
class DocumentLoadingState with _$DocumentLoadingState {
|
||||||
const factory DocumentLoadingState.loading() = _Loading;
|
const factory DocumentLoadingState.loading() = _Loading;
|
||||||
const factory DocumentLoadingState.finish(
|
const factory DocumentLoadingState.finish(
|
||||||
Either<FlowyError, DocumentDataPB2> successOrFail,
|
Either<FlowyError, DocumentDataPB> successOrFail,
|
||||||
) = _Finish;
|
) = _Finish;
|
||||||
}
|
}
|
||||||
|
@ -14,27 +14,27 @@ class DocumentService {
|
|||||||
if (canOpen.isRight()) {
|
if (canOpen.isRight()) {
|
||||||
return const Right(unit);
|
return const Right(unit);
|
||||||
}
|
}
|
||||||
final payload = CreateDocumentPayloadPBV2()..documentId = view.id;
|
final payload = CreateDocumentPayloadPB()..documentId = view.id;
|
||||||
final result = await DocumentEvent2CreateDocument(payload).send();
|
final result = await DocumentEventCreateDocument(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<FlowyError, DocumentDataPB2>> openDocument({
|
Future<Either<FlowyError, DocumentDataPB>> openDocument({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
}) async {
|
}) async {
|
||||||
// set the latest view
|
// set the latest view
|
||||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||||
|
|
||||||
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
|
final payload = OpenDocumentPayloadPB()..documentId = view.id;
|
||||||
final result = await DocumentEvent2OpenDocument(payload).send();
|
final result = await DocumentEventOpenDocument(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<FlowyError, Unit>> closeDocument({
|
Future<Either<FlowyError, Unit>> closeDocument({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
}) async {
|
}) async {
|
||||||
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
|
final payload = CloseDocumentPayloadPB()..documentId = view.id;
|
||||||
final result = await DocumentEvent2CloseDocument(payload).send();
|
final result = await DocumentEventCloseDocument(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,11 +42,11 @@ class DocumentService {
|
|||||||
required String documentId,
|
required String documentId,
|
||||||
required Iterable<BlockActionPB> actions,
|
required Iterable<BlockActionPB> actions,
|
||||||
}) async {
|
}) async {
|
||||||
final payload = ApplyActionPayloadPBV2(
|
final payload = ApplyActionPayloadPB(
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
);
|
);
|
||||||
final result = await DocumentEvent2ApplyAction(payload).send();
|
final result = await DocumentEventApplyAction(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,19 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
show Document, Node, Attributes, Delta, ParagraphBlockKeys;
|
show Document, Node, Attributes, Delta, ParagraphBlockKeys;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
extension AppFlowyEditor on DocumentDataPB2 {
|
extension AppFlowyEditor on DocumentDataPB {
|
||||||
|
DocumentDataPB? fromDocument(Document document) {
|
||||||
|
final blocks = <String, BlockPB>{};
|
||||||
|
final pageId = document.root.id;
|
||||||
|
final childrenMap = <String, ChildrenPB>{};
|
||||||
|
final meta = MetaPB(childrenMap: childrenMap);
|
||||||
|
return DocumentDataPB(
|
||||||
|
blocks: blocks,
|
||||||
|
pageId: pageId,
|
||||||
|
meta: meta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Document? toDocument() {
|
Document? toDocument() {
|
||||||
final rootId = pageId;
|
final rootId = pageId;
|
||||||
try {
|
try {
|
||||||
@ -78,12 +90,17 @@ extension BlockToNode on BlockPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension NodeToBlock on Node {
|
extension NodeToBlock on Node {
|
||||||
BlockPB toBlock() {
|
BlockPB toBlock({
|
||||||
|
String? childrenId,
|
||||||
|
}) {
|
||||||
assert(id.isNotEmpty);
|
assert(id.isNotEmpty);
|
||||||
final block = BlockPB.create()
|
final block = BlockPB.create()
|
||||||
..id = id
|
..id = id
|
||||||
..ty = _typeAdapter(type)
|
..ty = _typeAdapter(type)
|
||||||
..data = _dataAdapter(type, attributes);
|
..data = _dataAdapter(type, attributes);
|
||||||
|
if (childrenId != null && childrenId.isNotEmpty) {
|
||||||
|
block.childrenId = childrenId;
|
||||||
|
}
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
/// Uses to adjust the data structure between the editor and the backend.
|
/// Uses to adjust the data structure between the editor and the backend.
|
||||||
///
|
///
|
||||||
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
|
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
|
||||||
@ -64,14 +66,20 @@ extension on InsertOperation {
|
|||||||
for (final node in nodes) {
|
for (final node in nodes) {
|
||||||
final parentId =
|
final parentId =
|
||||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||||
final prevId = previousNode?.id ??
|
var prevId = previousNode?.id ??
|
||||||
editorState.getNodeAtPath(path.previous)?.id ??
|
editorState.getNodeAtPath(path.previous)?.id ??
|
||||||
'';
|
'';
|
||||||
assert(parentId.isNotEmpty && prevId.isNotEmpty);
|
assert(parentId.isNotEmpty);
|
||||||
|
if (path.equals(path.previous)) {
|
||||||
|
prevId = '';
|
||||||
|
} else {
|
||||||
|
assert(prevId.isNotEmpty && prevId != node.id);
|
||||||
|
}
|
||||||
final payload = BlockActionPayloadPB()
|
final payload = BlockActionPayloadPB()
|
||||||
..block = node.toBlock()
|
..block = node.toBlock(childrenId: nanoid(10))
|
||||||
..parentId = parentId
|
..parentId = parentId
|
||||||
..prevId = prevId;
|
..prevId = prevId;
|
||||||
|
assert(payload.block.childrenId.isNotEmpty);
|
||||||
actions.add(
|
actions.add(
|
||||||
BlockActionPB()
|
BlockActionPB()
|
||||||
..action = BlockActionTypePB.Insert
|
..action = BlockActionTypePB.Insert
|
||||||
|
@ -9,7 +9,8 @@ import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/base64_string.dart';
|
import 'package:appflowy/util/base64_string.dart';
|
||||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||||
|
hide DocumentEvent;
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
@ -119,7 +120,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _exportPage(DocumentDataPB2 data) async {
|
Future<void> _exportPage(DocumentDataPB data) async {
|
||||||
final picker = getIt<FilePickerService>();
|
final picker = getIt<FilePickerService>();
|
||||||
final dir = await picker.getDirectoryPath();
|
final dir = await picker.getDirectoryPath();
|
||||||
if (dir == null) {
|
if (dir == null) {
|
||||||
|
@ -225,6 +225,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
builder.showActions = (_) => true;
|
||||||
builder.actionBuilder = (context, state) => BlockActionList(
|
builder.actionBuilder = (context, state) => BlockActionList(
|
||||||
blockComponentContext: context,
|
blockComponentContext: context,
|
||||||
blockComponentState: state,
|
blockComponentState: state,
|
||||||
|
@ -31,10 +31,12 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
popupBuilder: (context) => _buildEmojiPicker(),
|
popupBuilder: (context) => _buildEmojiPicker(),
|
||||||
child: FlowyTextButton(
|
child: FlowyTextButton(
|
||||||
emoji,
|
emoji,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
fontSize: emojiSize,
|
fontSize: emojiSize,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 35.0),
|
constraints: const BoxConstraints(minWidth: 35.0),
|
||||||
fillColor: Colors.transparent,
|
fillColor: Colors.transparent,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
popoverController.show();
|
popoverController.show();
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../base/emoji_picker_button.dart';
|
import '../base/emoji_picker_button.dart';
|
||||||
@ -147,7 +148,9 @@ class _CalloutBlockComponentWidgetState
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2.0),
|
||||||
child: EmojiPickerButton(
|
child: EmojiPickerButton(
|
||||||
key: ValueKey(emoji), // force to refresh the popover state
|
key: ValueKey(
|
||||||
|
emoji.toString(),
|
||||||
|
), // force to refresh the popover state
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
onSubmitted: (emoji, controller) {
|
onSubmitted: (emoji, controller) {
|
||||||
setEmoji(emoji.emoji);
|
setEmoji(emoji.emoji);
|
||||||
@ -157,13 +160,11 @@ class _CalloutBlockComponentWidgetState
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: buildCalloutBlockComponent(context),
|
child: buildCalloutBlockComponent(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const VSpace(10),
|
||||||
width: 10.0,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -150,7 +150,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
fontColor: Theme.of(context).colorScheme.tertiary,
|
fontColor: Theme.of(context).colorScheme.tertiary,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final hasFileImageCover = CoverSelectionType.fromString(
|
final hasFileImageCover = CoverSelectionType.fromString(
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
) ==
|
) ==
|
||||||
CoverSelectionType.file;
|
CoverSelectionType.file;
|
||||||
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
||||||
@ -220,9 +220,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
pickerBackgroundColor: theme.cardColor,
|
pickerBackgroundColor: theme.cardColor,
|
||||||
pickerItemHoverColor: theme.hoverColor,
|
pickerItemHoverColor: theme.hoverColor,
|
||||||
selectedBackgroundColorHex:
|
selectedBackgroundColorHex:
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute] ==
|
widget.node.attributes[CoverBlockKeys.selectionType] ==
|
||||||
CoverSelectionType.color.toString()
|
CoverSelectionType.color.toString()
|
||||||
? widget.node.attributes[kCoverSelectionAttribute]
|
? widget.node.attributes[CoverBlockKeys.selection]
|
||||||
: 'ffffff',
|
: 'ffffff',
|
||||||
backgroundColorOptions:
|
backgroundColorOptions:
|
||||||
_generateBackgroundColorOptions(widget.editorState),
|
_generateBackgroundColorOptions(widget.editorState),
|
||||||
@ -284,7 +284,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
final changeCoverBloc =
|
final changeCoverBloc =
|
||||||
context.read<ChangeCoverPopoverBloc>();
|
context.read<ChangeCoverPopoverBloc>();
|
||||||
final deletingCurrentCover =
|
final deletingCurrentCover =
|
||||||
widget.node.attributes[kCoverSelectionAttribute] ==
|
widget.node.attributes[CoverBlockKeys.selection] ==
|
||||||
images[index - 1];
|
images[index - 1];
|
||||||
if (deletingCurrentCover) {
|
if (deletingCurrentCover) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
|
@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
|
|||||||
deleteImage: (DeleteImage deleteImage) async {
|
deleteImage: (DeleteImage deleteImage) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
final currentlySelectedImage =
|
final currentlySelectedImage =
|
||||||
node.attributes[kCoverSelectionAttribute];
|
node.attributes[CoverBlockKeys.selection];
|
||||||
if (currentState is Loaded) {
|
if (currentState is Loaded) {
|
||||||
await _deleteImageInStorage(deleteImage.path);
|
await _deleteImageInStorage(deleteImage.path);
|
||||||
if (currentlySelectedImage == deleteImage.path) {
|
if (currentlySelectedImage == deleteImage.path) {
|
||||||
@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
|
|||||||
clearAllImages: (ClearAllImages clearAllImages) async {
|
clearAllImages: (ClearAllImages clearAllImages) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
final currentlySelectedImage =
|
final currentlySelectedImage =
|
||||||
node.attributes[kCoverSelectionAttribute];
|
node.attributes[CoverBlockKeys.selection];
|
||||||
|
|
||||||
if (currentState is Loaded) {
|
if (currentState is Loaded) {
|
||||||
for (final image in currentState.imageNames) {
|
for (final image in currentState.imageNames) {
|
||||||
@ -90,8 +90,9 @@ class ChangeCoverPopoverBloc
|
|||||||
Future<void> _removeCoverImageFromNode() async {
|
Future<void> _removeCoverImageFromNode() async {
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
transaction.updateNode(node, {
|
transaction.updateNode(node, {
|
||||||
kCoverSelectionTypeAttribute: CoverSelectionType.initial.toString(),
|
CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
|
||||||
kIconSelectionAttribute: node.attributes[kIconSelectionAttribute]
|
CoverBlockKeys.iconSelection:
|
||||||
|
node.attributes[CoverBlockKeys.iconSelection]
|
||||||
});
|
});
|
||||||
return editorState.apply(transaction);
|
return editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,13 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const String kCoverType = 'cover';
|
class CoverBlockKeys {
|
||||||
const String kCoverSelectionTypeAttribute = 'cover_selection_type';
|
const CoverBlockKeys._();
|
||||||
const String kCoverSelectionAttribute = 'cover_selection';
|
|
||||||
const String kIconSelectionAttribute = 'selected_icon';
|
static const String selectionType = 'cover_selection_type';
|
||||||
|
static const String selection = 'cover_selection';
|
||||||
|
static const String iconSelection = 'selected_icon';
|
||||||
|
}
|
||||||
|
|
||||||
enum CoverSelectionType {
|
enum CoverSelectionType {
|
||||||
initial,
|
initial,
|
||||||
@ -69,7 +72,7 @@ class CoverImageNodeWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
||||||
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -105,9 +108,10 @@ class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
|||||||
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kCoverSelectionTypeAttribute: type.toString(),
|
CoverBlockKeys.selectionType: type.toString(),
|
||||||
kCoverSelectionAttribute: cover,
|
CoverBlockKeys.selection: cover,
|
||||||
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
CoverBlockKeys.iconSelection:
|
||||||
|
widget.node.attributes[CoverBlockKeys.iconSelection]
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -247,11 +251,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
|||||||
Future<void> _insertIcon(Emoji emoji) async {
|
Future<void> _insertIcon(Emoji emoji) async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kCoverSelectionTypeAttribute:
|
CoverBlockKeys.selectionType:
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
kCoverSelectionAttribute:
|
CoverBlockKeys.selection:
|
||||||
widget.node.attributes[kCoverSelectionAttribute],
|
widget.node.attributes[CoverBlockKeys.selection],
|
||||||
kIconSelectionAttribute: emoji.emoji,
|
CoverBlockKeys.iconSelection: emoji.emoji,
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -259,11 +263,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
|||||||
Future<void> _removeIcon() async {
|
Future<void> _removeIcon() async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kIconSelectionAttribute: "",
|
CoverBlockKeys.iconSelection: "",
|
||||||
kCoverSelectionTypeAttribute:
|
CoverBlockKeys.selectionType:
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
kCoverSelectionAttribute:
|
CoverBlockKeys.selection:
|
||||||
widget.node.attributes[kCoverSelectionAttribute],
|
widget.node.attributes[CoverBlockKeys.selection],
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -297,15 +301,16 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
final popoverController = PopoverController();
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
);
|
);
|
||||||
Color get color => Color(
|
Color get color => Color(
|
||||||
int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
|
int.tryParse(widget.node.attributes[CoverBlockKeys.selection]) ??
|
||||||
0xFFFFFFFF,
|
0xFFFFFFFF,
|
||||||
);
|
);
|
||||||
bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
|
bool get hasIcon =>
|
||||||
|
widget.node.attributes[CoverBlockKeys.iconSelection] == null
|
||||||
? false
|
? false
|
||||||
: widget.node.attributes[kIconSelectionAttribute].isNotEmpty;
|
: widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty;
|
||||||
bool isOverlayButtonsHidden = true;
|
bool isOverlayButtonsHidden = true;
|
||||||
PopoverController iconPopoverController = PopoverController();
|
PopoverController iconPopoverController = PopoverController();
|
||||||
bool get hasCover =>
|
bool get hasCover =>
|
||||||
@ -336,7 +341,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
constraints: BoxConstraints.loose(const Size(320, 380)),
|
constraints: BoxConstraints.loose(const Size(320, 380)),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: EmojiIconWidget(
|
child: EmojiIconWidget(
|
||||||
emoji: widget.node.attributes[kIconSelectionAttribute],
|
emoji: widget.node.attributes[CoverBlockKeys.iconSelection],
|
||||||
),
|
),
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
return EmojiPopover(
|
return EmojiPopover(
|
||||||
@ -375,9 +380,10 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kCoverSelectionTypeAttribute: type.toString(),
|
CoverBlockKeys.selectionType: type.toString(),
|
||||||
kCoverSelectionAttribute: cover,
|
CoverBlockKeys.selection: cover,
|
||||||
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
CoverBlockKeys.iconSelection:
|
||||||
|
widget.node.attributes[CoverBlockKeys.iconSelection]
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -385,11 +391,11 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
Future<void> _insertIcon(Emoji emoji) async {
|
Future<void> _insertIcon(Emoji emoji) async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kCoverSelectionTypeAttribute:
|
CoverBlockKeys.selectionType:
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
kCoverSelectionAttribute:
|
CoverBlockKeys.selection:
|
||||||
widget.node.attributes[kCoverSelectionAttribute],
|
widget.node.attributes[CoverBlockKeys.selection],
|
||||||
kIconSelectionAttribute: emoji.emoji,
|
CoverBlockKeys.iconSelection: emoji.emoji,
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -397,11 +403,11 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
Future<void> _removeIcon() async {
|
Future<void> _removeIcon() async {
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
transaction.updateNode(widget.node, {
|
transaction.updateNode(widget.node, {
|
||||||
kIconSelectionAttribute: "",
|
CoverBlockKeys.iconSelection: "",
|
||||||
kCoverSelectionTypeAttribute:
|
CoverBlockKeys.selectionType:
|
||||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||||
kCoverSelectionAttribute:
|
CoverBlockKeys.selection:
|
||||||
widget.node.attributes[kCoverSelectionAttribute],
|
widget.node.attributes[CoverBlockKeys.selection],
|
||||||
});
|
});
|
||||||
return widget.editorState.apply(transaction);
|
return widget.editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
@ -409,7 +415,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
Widget _buildCoverOverlayButtons(BuildContext context) {
|
Widget _buildCoverOverlayButtons(BuildContext context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
right: 260,
|
right: EditorStyleCustomizer.horizontalPadding,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -480,7 +486,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
switch (selectionType) {
|
switch (selectionType) {
|
||||||
case CoverSelectionType.file:
|
case CoverSelectionType.file:
|
||||||
final imageFile =
|
final imageFile =
|
||||||
File(widget.node.attributes[kCoverSelectionAttribute]);
|
File(widget.node.attributes[CoverBlockKeys.selection]);
|
||||||
if (!imageFile.existsSync()) {
|
if (!imageFile.existsSync()) {
|
||||||
// reset cover state
|
// reset cover state
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@ -496,7 +502,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|||||||
break;
|
break;
|
||||||
case CoverSelectionType.asset:
|
case CoverSelectionType.asset:
|
||||||
coverImage = Image.asset(
|
coverImage = Image.asset(
|
||||||
widget.node.attributes[kCoverSelectionAttribute],
|
widget.node.attributes[CoverBlockKeys.selection],
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class EditorMigration {
|
||||||
|
// AppFlowy 0.1.x -> 0.2
|
||||||
|
//
|
||||||
|
// The cover node has been deprecated, and use page/attributes/cover instead.
|
||||||
|
// cover node -> page/attributes/cover
|
||||||
|
//
|
||||||
|
// mark the textNode deprecated. use paragraph node instead.
|
||||||
|
// text node -> paragraph node
|
||||||
|
// delta -> attributes/delta
|
||||||
|
//
|
||||||
|
// mark the subtype deprecated. use type instead.
|
||||||
|
// for example, text/checkbox -> checkbox_list
|
||||||
|
//
|
||||||
|
// some attribute keys.
|
||||||
|
// ...
|
||||||
|
static Document migrateDocument(String json) {
|
||||||
|
final map = jsonDecode(json);
|
||||||
|
assert(map['document'] != null);
|
||||||
|
final documentV0 = Map<String, Object>.from(map['document'] as Map);
|
||||||
|
final rootV0 = NodeV0.fromJson(documentV0);
|
||||||
|
final root = migrateNode(rootV0);
|
||||||
|
return Document(root: root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node migrateNode(NodeV0 nodeV0) {
|
||||||
|
Node? node;
|
||||||
|
final children = nodeV0.children.map((e) => migrateNode(e)).toList();
|
||||||
|
final id = nodeV0.id;
|
||||||
|
if (id == 'editor') {
|
||||||
|
final coverNode = children.firstWhereOrNull(
|
||||||
|
(element) => element.id == 'cover',
|
||||||
|
);
|
||||||
|
if (coverNode != null) {
|
||||||
|
node = documentNode(
|
||||||
|
children: children,
|
||||||
|
attributes: coverNode.attributes,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
node = documentNode(children: children);
|
||||||
|
}
|
||||||
|
} else if (id == 'callout') {
|
||||||
|
final emoji = nodeV0.attributes['emoji'] ?? '📌';
|
||||||
|
final delta =
|
||||||
|
nodeV0.children.whereType<TextNodeV0>().fold(Delta(), (p, e) {
|
||||||
|
final delta = migrateDelta(e.delta);
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
for (final element in textInserts) {
|
||||||
|
p.add(element);
|
||||||
|
}
|
||||||
|
return p..insert('\n');
|
||||||
|
});
|
||||||
|
node = calloutNode(
|
||||||
|
emoji: emoji,
|
||||||
|
delta: delta,
|
||||||
|
);
|
||||||
|
} else if (id == 'divider') {
|
||||||
|
// divider -> divider
|
||||||
|
node = dividerNode();
|
||||||
|
} else if (id == 'math_equation') {
|
||||||
|
// math_equation -> math_equation
|
||||||
|
final formula = nodeV0.attributes['math_equation'] ?? '';
|
||||||
|
node = mathEquationNode(formula: formula);
|
||||||
|
} else if (nodeV0 is TextNodeV0) {
|
||||||
|
final delta = migrateDelta(nodeV0.delta);
|
||||||
|
final deltaJson = delta.toJson();
|
||||||
|
final attributes = {'delta': deltaJson};
|
||||||
|
if (id == 'text') {
|
||||||
|
// text -> paragraph
|
||||||
|
node = paragraphNode(
|
||||||
|
attributes: attributes,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
} else if (nodeV0.id == 'text/heading') {
|
||||||
|
// text/heading -> heading
|
||||||
|
final heading = nodeV0.attributes.heading?.replaceAll('h', '');
|
||||||
|
final level = int.tryParse(heading ?? '') ?? 1;
|
||||||
|
node = headingNode(
|
||||||
|
level: level,
|
||||||
|
attributes: attributes,
|
||||||
|
);
|
||||||
|
} else if (id == 'text/checkbox') {
|
||||||
|
// text/checkbox -> todo_list
|
||||||
|
final checked = nodeV0.attributes.check;
|
||||||
|
node = todoListNode(
|
||||||
|
checked: checked,
|
||||||
|
attributes: attributes,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
} else if (id == 'text/quote') {
|
||||||
|
// text/quote -> quote
|
||||||
|
node = quoteNode(attributes: attributes);
|
||||||
|
} else if (id == 'text/number-list') {
|
||||||
|
// text/number-list -> numbered_list
|
||||||
|
node = numberedListNode(
|
||||||
|
attributes: attributes,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
} else if (id == 'text/bulleted-list') {
|
||||||
|
// text/bulleted-list -> bulleted_list
|
||||||
|
node = bulletedListNode(
|
||||||
|
attributes: attributes,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
} else if (id == 'text/code_block') {
|
||||||
|
// text/code_block -> code
|
||||||
|
final language = nodeV0.attributes['language'];
|
||||||
|
node = codeBlockNode(delta: delta, language: language);
|
||||||
|
}
|
||||||
|
} else if (id == 'cover') {
|
||||||
|
node = paragraphNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return node ?? paragraphNode(text: jsonEncode(nodeV0.toJson()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate the attributes.
|
||||||
|
// backgroundColor -> highlightColor
|
||||||
|
// color -> textColor
|
||||||
|
static Delta migrateDelta(Delta delta) {
|
||||||
|
final textInserts = delta
|
||||||
|
.whereType<TextInsert>()
|
||||||
|
.map(
|
||||||
|
(e) => TextInsert(
|
||||||
|
e.text,
|
||||||
|
attributes: migrateAttributes(e.attributes),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
return Delta(operations: textInserts.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Attributes? migrateAttributes(Attributes? attributes) {
|
||||||
|
if (attributes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const backgroundColor = 'backgroundColor';
|
||||||
|
if (attributes.containsKey(backgroundColor)) {
|
||||||
|
attributes['highlightColor'] = attributes[backgroundColor];
|
||||||
|
attributes.remove(backgroundColor);
|
||||||
|
}
|
||||||
|
const color = 'color';
|
||||||
|
if (attributes.containsKey(color)) {
|
||||||
|
attributes['textColor'] = attributes[color];
|
||||||
|
attributes.remove(color);
|
||||||
|
}
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ class AutoCompletionBlockKeys {
|
|||||||
static const String type = 'auto_completion';
|
static const String type = 'auto_completion';
|
||||||
static const String prompt = 'prompt';
|
static const String prompt = 'prompt';
|
||||||
static const String startSelection = 'start_selection';
|
static const String startSelection = 'start_selection';
|
||||||
|
static const String generationCount = 'generation_count';
|
||||||
}
|
}
|
||||||
|
|
||||||
Node autoCompletionNode({
|
Node autoCompletionNode({
|
||||||
@ -35,6 +36,7 @@ Node autoCompletionNode({
|
|||||||
attributes: {
|
attributes: {
|
||||||
AutoCompletionBlockKeys.prompt: prompt,
|
AutoCompletionBlockKeys.prompt: prompt,
|
||||||
AutoCompletionBlockKeys.startSelection: start.toJson(),
|
AutoCompletionBlockKeys.startSelection: start.toJson(),
|
||||||
|
AutoCompletionBlockKeys.generationCount: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -92,6 +94,16 @@ class _AutoCompletionBlockComponentState
|
|||||||
late final SelectionGestureInterceptor interceptor;
|
late final SelectionGestureInterceptor interceptor;
|
||||||
|
|
||||||
String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt];
|
String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt];
|
||||||
|
int get generationCount =>
|
||||||
|
widget.node.attributes[AutoCompletionBlockKeys.generationCount] ?? 0;
|
||||||
|
Selection? get startSelection {
|
||||||
|
final selection =
|
||||||
|
widget.node.attributes[AutoCompletionBlockKeys.startSelection];
|
||||||
|
if (selection != null) {
|
||||||
|
return Selection.fromJson(selection);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -106,6 +118,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_onExit();
|
||||||
_unsubscribeSelectionGesture();
|
_unsubscribeSelectionGesture();
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
|
|
||||||
@ -124,7 +137,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
children: [
|
children: [
|
||||||
const AutoCompletionHeader(),
|
const AutoCompletionHeader(),
|
||||||
const Space(0, 10),
|
const Space(0, 10),
|
||||||
if (prompt.isEmpty) ...[
|
if (prompt.isEmpty && generationCount < 1) ...[
|
||||||
_buildInputWidget(context),
|
_buildInputWidget(context),
|
||||||
const Space(0, 10),
|
const Space(0, 10),
|
||||||
AutoCompletionInputFooter(
|
AutoCompletionInputFooter(
|
||||||
@ -134,6 +147,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
] else ...[
|
] else ...[
|
||||||
AutoCompletionFooter(
|
AutoCompletionFooter(
|
||||||
onKeep: _onExit,
|
onKeep: _onExit,
|
||||||
|
onRewrite: _onRewrite,
|
||||||
onDiscard: _onDiscard,
|
onDiscard: _onDiscard,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -213,13 +227,39 @@ class _AutoCompletionBlockComponentState
|
|||||||
await _showError(error.message);
|
await _showError(error.message);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await _updateGenerationCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDiscard() async {
|
Future<void> _onDiscard() async {
|
||||||
final selection =
|
final selection = startSelection;
|
||||||
widget.node.attributes[AutoCompletionBlockKeys.startSelection];
|
|
||||||
if (selection != null) {
|
if (selection != null) {
|
||||||
final start = Selection.fromJson(selection).start.path;
|
final start = selection.start.path;
|
||||||
|
final end = widget.node.previous?.path;
|
||||||
|
if (end != null) {
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.deleteNodesAtPath(
|
||||||
|
start,
|
||||||
|
end.last - start.last + 1,
|
||||||
|
);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
await _makeSurePreviousNodeIsEmptyParagraphNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRewrite() async {
|
||||||
|
final previousOutput = _getPreviousOutput();
|
||||||
|
if (previousOutput == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final loading = Loading(context);
|
||||||
|
loading.start();
|
||||||
|
// clear previous response
|
||||||
|
final selection = startSelection;
|
||||||
|
if (selection != null) {
|
||||||
|
final start = selection.start.path;
|
||||||
final end = widget.node.previous?.path;
|
final end = widget.node.previous?.path;
|
||||||
if (end != null) {
|
if (end != null) {
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
@ -230,7 +270,71 @@ class _AutoCompletionBlockComponentState
|
|||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_onExit();
|
// generate new response
|
||||||
|
final userProfile = await UserBackendService.getCurrentUserProfile()
|
||||||
|
.then((value) => value.toOption().toNullable());
|
||||||
|
if (userProfile == null) {
|
||||||
|
loading.stop();
|
||||||
|
await _showError(
|
||||||
|
LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final textRobot = TextRobot(editorState: editorState);
|
||||||
|
final openAIRepository = HttpOpenAIRepository(
|
||||||
|
client: http.Client(),
|
||||||
|
apiKey: userProfile.openaiKey,
|
||||||
|
);
|
||||||
|
await openAIRepository.getStreamedCompletions(
|
||||||
|
prompt: _rewritePrompt(previousOutput),
|
||||||
|
onStart: () async {
|
||||||
|
loading.stop();
|
||||||
|
await _makeSurePreviousNodeIsEmptyParagraphNode();
|
||||||
|
},
|
||||||
|
onProcess: (response) async {
|
||||||
|
if (response.choices.isNotEmpty) {
|
||||||
|
final text = response.choices.first.text;
|
||||||
|
await textRobot.autoInsertText(
|
||||||
|
text,
|
||||||
|
inputType: TextRobotInputType.word,
|
||||||
|
delay: Duration.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEnd: () async {},
|
||||||
|
onError: (error) async {
|
||||||
|
loading.stop();
|
||||||
|
await _showError(error.message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await _updateGenerationCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getPreviousOutput() {
|
||||||
|
final startSelection = this.startSelection;
|
||||||
|
if (startSelection != null) {
|
||||||
|
final end = widget.node.previous?.path;
|
||||||
|
|
||||||
|
if (end != null) {
|
||||||
|
final result = editorState
|
||||||
|
.getNodesInSelection(
|
||||||
|
startSelection.copyWith(end: Position(path: end)),
|
||||||
|
)
|
||||||
|
.fold(
|
||||||
|
'',
|
||||||
|
(previousValue, element) {
|
||||||
|
final delta = element.delta;
|
||||||
|
if (delta != null) {
|
||||||
|
return "$previousValue\n${delta.toPlainText()}";
|
||||||
|
} else {
|
||||||
|
return previousValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateEditingText() async {
|
Future<void> _updateEditingText() async {
|
||||||
@ -244,6 +348,21 @@ class _AutoCompletionBlockComponentState
|
|||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateGenerationCount() async {
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.updateNode(
|
||||||
|
widget.node,
|
||||||
|
{
|
||||||
|
AutoCompletionBlockKeys.generationCount: generationCount + 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _rewritePrompt(String previousOutput) {
|
||||||
|
return 'I am not satisfied with your previous response ($previousOutput) to the query ($prompt). Please provide an alternative response.';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _makeSurePreviousNodeIsEmptyParagraphNode() async {
|
Future<void> _makeSurePreviousNodeIsEmptyParagraphNode() async {
|
||||||
// make sure the previous node is a empty paragraph node without any styles.
|
// make sure the previous node is a empty paragraph node without any styles.
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
@ -393,10 +512,12 @@ class AutoCompletionFooter extends StatelessWidget {
|
|||||||
const AutoCompletionFooter({
|
const AutoCompletionFooter({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onKeep,
|
required this.onKeep,
|
||||||
|
required this.onRewrite,
|
||||||
required this.onDiscard,
|
required this.onDiscard,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback onKeep;
|
final VoidCallback onKeep;
|
||||||
|
final VoidCallback onRewrite;
|
||||||
final VoidCallback onDiscard;
|
final VoidCallback onDiscard;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -408,6 +529,11 @@ class AutoCompletionFooter extends StatelessWidget {
|
|||||||
onPressed: onKeep,
|
onPressed: onKeep,
|
||||||
),
|
),
|
||||||
const Space(10, 0),
|
const Space(10, 0),
|
||||||
|
SecondaryTextButton(
|
||||||
|
LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||||
|
onPressed: onRewrite,
|
||||||
|
),
|
||||||
|
const Space(10, 0),
|
||||||
SecondaryTextButton(
|
SecondaryTextButton(
|
||||||
LocaleKeys.button_discard.tr(),
|
LocaleKeys.button_discard.tr(),
|
||||||
onPressed: onDiscard,
|
onPressed: onDiscard,
|
||||||
|
@ -15,10 +15,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
const String kSmartEditType = 'smart_edit_input';
|
|
||||||
const String kSmartEditInstructionType = 'smart_edit_instruction';
|
|
||||||
const String kSmartEditInputType = 'smart_edit_input';
|
|
||||||
|
|
||||||
class SmartEditBlockKeys {
|
class SmartEditBlockKeys {
|
||||||
const SmartEditBlockKeys._();
|
const SmartEditBlockKeys._();
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class EditorStyleCustomizer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
static double get horizontalPadding =>
|
static double get horizontalPadding =>
|
||||||
PlatformExtension.isDesktop ? 100.0 : 10.0;
|
PlatformExtension.isDesktop ? 50.0 : 10.0;
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
@ -28,21 +28,18 @@ class EditorStyleCustomizer {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||||
return EditorStyle.desktop(
|
return EditorStyle.desktop(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||||
left: horizontalPadding / 2.0,
|
|
||||||
right: horizontalPadding,
|
|
||||||
),
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
cursorColor: theme.colorScheme.primary,
|
cursorColor: theme.colorScheme.primary,
|
||||||
textStyleConfiguration: TextStyleConfiguration(
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
text: TextStyle(
|
text: TextStyle(
|
||||||
fontFamily: 'poppins',
|
fontFamily: 'Poppins',
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
color: theme.colorScheme.onBackground,
|
color: theme.colorScheme.onBackground,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
bold: const TextStyle(
|
bold: const TextStyle(
|
||||||
fontFamily: 'poppins-Bold',
|
fontFamily: 'Poppins-Bold',
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
|
||||||
@ -15,24 +16,20 @@ class ExportPageWidget extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const FlowyText.regular(
|
const FlowyText.medium(
|
||||||
'There are some errors.',
|
'Open document failed',
|
||||||
fontSize: 16.0,
|
fontSize: 18.0,
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
),
|
||||||
|
const VSpace(5),
|
||||||
const FlowyText.regular(
|
const FlowyText.regular(
|
||||||
'Please try to export the page and contact us.',
|
'Please try to export the page and contact us.',
|
||||||
fontSize: 14.0,
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const VSpace(20),
|
||||||
height: 5,
|
RoundedTextButton(
|
||||||
),
|
title: 'Export page',
|
||||||
FlowyTextButton(
|
width: 100,
|
||||||
'Export page',
|
height: 30,
|
||||||
constraints: const BoxConstraints(maxWidth: 100),
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: b1a1b14
|
ref: ead61af
|
||||||
resolved-ref: b1a1b14f35114a7becdb3e2de909d546d7328a59
|
resolved-ref: ead61afb796037e8ceb63ba4bcf439818514ed4b
|
||||||
url: "https://github.com/LucasXu0/appflowy-editor.git"
|
url: "https://github.com/LucasXu0/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.12"
|
version: "0.1.12"
|
||||||
|
@ -47,7 +47,7 @@ dependencies:
|
|||||||
# path: /Users/lucas.xu/Desktop/appflowy-editor
|
# path: /Users/lucas.xu/Desktop/appflowy-editor
|
||||||
git:
|
git:
|
||||||
url: https://github.com/LucasXu0/appflowy-editor.git
|
url: https://github.com/LucasXu0/appflowy-editor.git
|
||||||
ref: b1a1b14
|
ref: ead61af
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
@ -179,6 +179,7 @@ flutter:
|
|||||||
- assets/images/login/
|
- assets/images/login/
|
||||||
- assets/images/grid/setting/
|
- assets/images/grid/setting/
|
||||||
- assets/translations/
|
- assets/translations/
|
||||||
|
- assets/template/readme.json
|
||||||
|
|
||||||
# The following assets will be excluded in release.
|
# The following assets will be excluded in release.
|
||||||
# BEGIN: EXCLUDE_IN_RELEASE
|
# BEGIN: EXCLUDE_IN_RELEASE
|
||||||
|
@ -1,49 +1,49 @@
|
|||||||
import {
|
import {
|
||||||
FlowyError,
|
FlowyError,
|
||||||
DocumentDataPB2,
|
DocumentDataPB,
|
||||||
OpenDocumentPayloadPBV2,
|
OpenDocumentPayloadPB,
|
||||||
CreateDocumentPayloadPBV2,
|
CreateDocumentPayloadPB,
|
||||||
ApplyActionPayloadPBV2,
|
ApplyActionPayloadPB,
|
||||||
BlockActionPB,
|
BlockActionPB,
|
||||||
CloseDocumentPayloadPBV2,
|
CloseDocumentPayloadPB,
|
||||||
} from '@/services/backend';
|
} from '@/services/backend';
|
||||||
import { Result } from 'ts-results';
|
import { Result } from 'ts-results';
|
||||||
import {
|
import {
|
||||||
DocumentEvent2ApplyAction,
|
DocumentEventApplyAction,
|
||||||
DocumentEvent2CloseDocument,
|
DocumentEventCloseDocument,
|
||||||
DocumentEvent2OpenDocument,
|
DocumentEventOpenDocument,
|
||||||
DocumentEvent2CreateDocument,
|
DocumentEventCreateDocument,
|
||||||
} from '@/services/backend/events/flowy-document2';
|
} from '@/services/backend/events/flowy-document2';
|
||||||
|
|
||||||
export class DocumentBackendService {
|
export class DocumentBackendService {
|
||||||
constructor(public readonly viewId: string) {}
|
constructor(public readonly viewId: string) {}
|
||||||
|
|
||||||
create = (): Promise<Result<void, FlowyError>> => {
|
create = (): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = CreateDocumentPayloadPBV2.fromObject({
|
const payload = CreateDocumentPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
return DocumentEvent2CreateDocument(payload);
|
return DocumentEventCreateDocument(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||||
const payload = OpenDocumentPayloadPBV2.fromObject({
|
const payload = OpenDocumentPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
return DocumentEvent2OpenDocument(payload);
|
return DocumentEventOpenDocument(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
applyActions = (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]): Promise<Result<void, FlowyError>> => {
|
applyActions = (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = ApplyActionPayloadPBV2.fromObject({
|
const payload = ApplyActionPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
});
|
});
|
||||||
return DocumentEvent2ApplyAction(payload);
|
return DocumentEventApplyAction(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
close = (): Promise<Result<void, FlowyError>> => {
|
close = (): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = CloseDocumentPayloadPBV2.fromObject({
|
const payload = CloseDocumentPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
return DocumentEvent2CloseDocument(payload);
|
return DocumentEventCloseDocument(payload);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
1
frontend/rust-lib/Cargo.lock
generated
1
frontend/rust-lib/Cargo.lock
generated
@ -1647,6 +1647,7 @@ dependencies = [
|
|||||||
name = "flowy-document2"
|
name = "flowy-document2"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"appflowy-integrate",
|
"appflowy-integrate",
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
|
@ -25,6 +25,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = {version = "1.0"}
|
serde_json = {version = "1.0"}
|
||||||
tracing = { version = "0.1", features = ["log"] }
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
tokio = { version = "1.26", features = ["full"] }
|
tokio = { version = "1.26", features = ["full"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.4.0"
|
tempfile = "3.4.0"
|
||||||
|
@ -3,12 +3,12 @@ use std::{collections::HashMap, vec};
|
|||||||
use collab_document::blocks::{Block, DocumentData, DocumentMeta};
|
use collab_document::blocks::{Block, DocumentData, DocumentMeta};
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
|
|
||||||
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
|
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DocumentDataWrapper(pub DocumentData);
|
pub struct DocumentDataWrapper(pub DocumentData);
|
||||||
|
|
||||||
impl From<DocumentDataWrapper> for DocumentDataPB2 {
|
impl From<DocumentDataWrapper> for DocumentDataPB {
|
||||||
fn from(data: DocumentDataWrapper) -> Self {
|
fn from(data: DocumentDataWrapper) -> Self {
|
||||||
let blocks = data
|
let blocks = data
|
||||||
.0
|
.0
|
||||||
@ -35,6 +35,31 @@ impl From<DocumentDataWrapper> for DocumentDataPB2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<DocumentDataPB> for DocumentDataWrapper {
|
||||||
|
fn from(data: DocumentDataPB) -> Self {
|
||||||
|
let blocks = data
|
||||||
|
.blocks
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, block)| (id, block.into()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let children_map = data
|
||||||
|
.meta
|
||||||
|
.children_map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, children)| (id, children.children))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let page_id = data.page_id;
|
||||||
|
|
||||||
|
Self(DocumentData {
|
||||||
|
page_id,
|
||||||
|
blocks,
|
||||||
|
meta: DocumentMeta { children_map },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// the default document data contains a page block and a text block
|
// the default document data contains a page block and a text block
|
||||||
impl Default for DocumentDataWrapper {
|
impl Default for DocumentDataWrapper {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -3,28 +3,28 @@ use std::collections::HashMap;
|
|||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct OpenDocumentPayloadPBV2 {
|
pub struct OpenDocumentPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
// Support customize initial data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct CreateDocumentPayloadPBV2 {
|
pub struct CreateDocumentPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
// Support customize initial data
|
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub initial_data: Option<DocumentDataPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct CloseDocumentPayloadPBV2 {
|
pub struct CloseDocumentPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
// Support customize initial data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct ApplyActionPayloadPBV2 {
|
pub struct ApplyActionPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
|
|
||||||
@ -33,14 +33,14 @@ pub struct ApplyActionPayloadPBV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct GetDocumentDataPayloadPBV2 {
|
pub struct GetDocumentDataPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
// Support customize initial data
|
// Support customize initial data
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct DocumentDataPB2 {
|
pub struct DocumentDataPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub page_id: String,
|
pub page_id: String,
|
||||||
|
|
||||||
|
@ -17,38 +17,40 @@ use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataRes
|
|||||||
use crate::{
|
use crate::{
|
||||||
document_data::DocumentDataWrapper,
|
document_data::DocumentDataWrapper,
|
||||||
entities::{
|
entities::{
|
||||||
ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
||||||
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPBV2, CreateDocumentPayloadPBV2, DeltaTypePB,
|
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, CreateDocumentPayloadPB, DeltaTypePB,
|
||||||
DocEventPB, DocumentDataPB2, OpenDocumentPayloadPBV2,
|
DocEventPB, DocumentDataPB, OpenDocumentPayloadPB,
|
||||||
},
|
},
|
||||||
manager::DocumentManager,
|
manager::DocumentManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handler for creating a new document
|
// Handler for creating a new document
|
||||||
pub(crate) async fn create_document_handler(
|
pub(crate) async fn create_document_handler(
|
||||||
data: AFPluginData<CreateDocumentPayloadPBV2>,
|
data: AFPluginData<CreateDocumentPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DocumentManager>>,
|
manager: AFPluginState<Arc<DocumentManager>>,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let context = data.into_inner();
|
let context = data.into_inner();
|
||||||
// Create a new document with a default content, one page block and one text block
|
let initial_data: DocumentDataWrapper = context
|
||||||
let data = DocumentDataWrapper::default();
|
.initial_data
|
||||||
manager.create_document(context.document_id, data)?;
|
.map(|data| data.into())
|
||||||
|
.unwrap_or_default();
|
||||||
|
manager.create_document(context.document_id, initial_data)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for opening an existing document
|
// Handler for opening an existing document
|
||||||
pub(crate) async fn open_document_handler(
|
pub(crate) async fn open_document_handler(
|
||||||
data: AFPluginData<OpenDocumentPayloadPBV2>,
|
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DocumentManager>>,
|
manager: AFPluginState<Arc<DocumentManager>>,
|
||||||
) -> DataResult<DocumentDataPB2, FlowyError> {
|
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||||
let context = data.into_inner();
|
let context = data.into_inner();
|
||||||
let document = manager.open_document(context.document_id)?;
|
let document = manager.open_document(context.document_id)?;
|
||||||
let document_data = document.lock().get_document()?;
|
let document_data = document.lock().get_document()?;
|
||||||
data_result_ok(DocumentDataPB2::from(DocumentDataWrapper(document_data)))
|
data_result_ok(DocumentDataPB::from(DocumentDataWrapper(document_data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn close_document_handler(
|
pub(crate) async fn close_document_handler(
|
||||||
data: AFPluginData<CloseDocumentPayloadPBV2>,
|
data: AFPluginData<CloseDocumentPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DocumentManager>>,
|
manager: AFPluginState<Arc<DocumentManager>>,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let context = data.into_inner();
|
let context = data.into_inner();
|
||||||
@ -59,18 +61,18 @@ pub(crate) async fn close_document_handler(
|
|||||||
// Get the content of the existing document,
|
// Get the content of the existing document,
|
||||||
// if the document does not exist, return an error.
|
// if the document does not exist, return an error.
|
||||||
pub(crate) async fn get_document_data_handler(
|
pub(crate) async fn get_document_data_handler(
|
||||||
data: AFPluginData<OpenDocumentPayloadPBV2>,
|
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DocumentManager>>,
|
manager: AFPluginState<Arc<DocumentManager>>,
|
||||||
) -> DataResult<DocumentDataPB2, FlowyError> {
|
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||||
let context = data.into_inner();
|
let context = data.into_inner();
|
||||||
let document = manager.get_document(context.document_id)?;
|
let document = manager.get_document(context.document_id)?;
|
||||||
let document_data = document.lock().get_document()?;
|
let document_data = document.lock().get_document()?;
|
||||||
data_result_ok(DocumentDataPB2::from(DocumentDataWrapper(document_data)))
|
data_result_ok(DocumentDataPB::from(DocumentDataWrapper(document_data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for applying an action to a document
|
// Handler for applying an action to a document
|
||||||
pub(crate) async fn apply_action_handler(
|
pub(crate) async fn apply_action_handler(
|
||||||
data: AFPluginData<ApplyActionPayloadPBV2>,
|
data: AFPluginData<ApplyActionPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DocumentManager>>,
|
manager: AFPluginState<Arc<DocumentManager>>,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let context = data.into_inner();
|
let context = data.into_inner();
|
||||||
|
@ -17,30 +17,30 @@ pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
|
|||||||
.name(env!("CARGO_PKG_NAME"))
|
.name(env!("CARGO_PKG_NAME"))
|
||||||
.state(document_manager);
|
.state(document_manager);
|
||||||
|
|
||||||
plugin = plugin.event(DocumentEvent2::OpenDocument, open_document_handler);
|
plugin = plugin.event(DocumentEvent::CreateDocument, create_document_handler);
|
||||||
plugin = plugin.event(DocumentEvent2::CloseDocument, close_document_handler);
|
plugin = plugin.event(DocumentEvent::OpenDocument, open_document_handler);
|
||||||
plugin = plugin.event(DocumentEvent2::ApplyAction, apply_action_handler);
|
plugin = plugin.event(DocumentEvent::CloseDocument, close_document_handler);
|
||||||
plugin = plugin.event(DocumentEvent2::CreateDocument, create_document_handler);
|
plugin = plugin.event(DocumentEvent::ApplyAction, apply_action_handler);
|
||||||
plugin = plugin.event(DocumentEvent2::GetDocumentData, get_document_data_handler);
|
plugin = plugin.event(DocumentEvent::GetDocumentData, get_document_data_handler);
|
||||||
|
|
||||||
plugin
|
plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||||
#[event_err = "FlowyError"]
|
#[event_err = "FlowyError"]
|
||||||
pub enum DocumentEvent2 {
|
pub enum DocumentEvent {
|
||||||
#[event(input = "CreateDocumentPayloadPBV2")]
|
#[event(input = "CreateDocumentPayloadPB")]
|
||||||
CreateDocument = 0,
|
CreateDocument = 0,
|
||||||
|
|
||||||
#[event(input = "OpenDocumentPayloadPBV2", output = "DocumentDataPB2")]
|
#[event(input = "OpenDocumentPayloadPB", output = "DocumentDataPB")]
|
||||||
OpenDocument = 1,
|
OpenDocument = 1,
|
||||||
|
|
||||||
#[event(input = "CloseDocumentPayloadPBV2")]
|
#[event(input = "CloseDocumentPayloadPB")]
|
||||||
CloseDocument = 2,
|
CloseDocument = 2,
|
||||||
|
|
||||||
#[event(input = "ApplyActionPayloadPBV2")]
|
#[event(input = "ApplyActionPayloadPB")]
|
||||||
ApplyAction = 3,
|
ApplyAction = 3,
|
||||||
|
|
||||||
#[event(input = "GetDocumentDataPayloadPBV2")]
|
#[event(input = "GetDocumentDataPayloadPB")]
|
||||||
GetDocumentData = 4,
|
GetDocumentData = 4,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user