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:
Lucas.Xu 2023-05-23 16:13:12 +08:00 committed by GitHub
parent f8d09e4894
commit ffff628359
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 757 additions and 160 deletions

View 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": []
}
]
}
}

View File

@ -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;
} }

View File

@ -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();
} }
} }

View File

@ -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;
} }

View File

@ -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

View File

@ -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) {

View File

@ -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,

View File

@ -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();
}, },

View File

@ -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,
)
], ],
), ),
); );

View File

@ -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(

View File

@ -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);
} }

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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._();

View File

@ -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),

View File

@ -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,
) )
], ],

View File

@ -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"

View File

@ -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

View File

@ -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);
}; };
} }

View File

@ -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",

View File

@ -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"

View File

@ -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 {

View File

@ -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,

View File

@ -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();

View File

@ -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,
} }