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/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/user/application/user_service.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-folder2/view.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.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) {
|
||||
prettyPrintJson(data.toProto3Json());
|
||||
}
|
||||
@ -140,7 +142,11 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
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;
|
||||
|
||||
// subscribe to the document change from the editor
|
||||
@ -189,6 +195,6 @@ class DocumentState with _$DocumentState {
|
||||
class DocumentLoadingState with _$DocumentLoadingState {
|
||||
const factory DocumentLoadingState.loading() = _Loading;
|
||||
const factory DocumentLoadingState.finish(
|
||||
Either<FlowyError, DocumentDataPB2> successOrFail,
|
||||
Either<FlowyError, DocumentDataPB> successOrFail,
|
||||
) = _Finish;
|
||||
}
|
||||
|
@ -14,27 +14,27 @@ class DocumentService {
|
||||
if (canOpen.isRight()) {
|
||||
return const Right(unit);
|
||||
}
|
||||
final payload = CreateDocumentPayloadPBV2()..documentId = view.id;
|
||||
final result = await DocumentEvent2CreateDocument(payload).send();
|
||||
final payload = CreateDocumentPayloadPB()..documentId = view.id;
|
||||
final result = await DocumentEventCreateDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<FlowyError, DocumentDataPB2>> openDocument({
|
||||
Future<Either<FlowyError, DocumentDataPB>> openDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
// set the latest view
|
||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||
|
||||
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
|
||||
final result = await DocumentEvent2OpenDocument(payload).send();
|
||||
final payload = OpenDocumentPayloadPB()..documentId = view.id;
|
||||
final result = await DocumentEventOpenDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<FlowyError, Unit>> closeDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
|
||||
final result = await DocumentEvent2CloseDocument(payload).send();
|
||||
final payload = CloseDocumentPayloadPB()..documentId = view.id;
|
||||
final result = await DocumentEventCloseDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
@ -42,11 +42,11 @@ class DocumentService {
|
||||
required String documentId,
|
||||
required Iterable<BlockActionPB> actions,
|
||||
}) async {
|
||||
final payload = ApplyActionPayloadPBV2(
|
||||
final payload = ApplyActionPayloadPB(
|
||||
documentId: documentId,
|
||||
actions: actions,
|
||||
);
|
||||
final result = await DocumentEvent2ApplyAction(payload).send();
|
||||
final result = await DocumentEventApplyAction(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,19 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show Document, Node, Attributes, Delta, ParagraphBlockKeys;
|
||||
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() {
|
||||
final rootId = pageId;
|
||||
try {
|
||||
@ -78,12 +90,17 @@ extension BlockToNode on BlockPB {
|
||||
}
|
||||
|
||||
extension NodeToBlock on Node {
|
||||
BlockPB toBlock() {
|
||||
BlockPB toBlock({
|
||||
String? childrenId,
|
||||
}) {
|
||||
assert(id.isNotEmpty);
|
||||
final block = BlockPB.create()
|
||||
..id = id
|
||||
..ty = _typeAdapter(type)
|
||||
..data = _dataAdapter(type, attributes);
|
||||
if (childrenId != null && childrenId.isNotEmpty) {
|
||||
block.childrenId = childrenId;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
/// 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.
|
||||
@ -64,14 +66,20 @@ extension on InsertOperation {
|
||||
for (final node in nodes) {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
final prevId = previousNode?.id ??
|
||||
var prevId = previousNode?.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()
|
||||
..block = node.toBlock()
|
||||
..block = node.toBlock(childrenId: nanoid(10))
|
||||
..parentId = parentId
|
||||
..prevId = prevId;
|
||||
assert(payload.block.childrenId.isNotEmpty);
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..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/util/base64_string.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_editor/appflowy_editor.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 dir = await picker.getDirectoryPath();
|
||||
if (dir == null) {
|
||||
|
@ -225,6 +225,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||
];
|
||||
|
||||
builder.showActions = (_) => true;
|
||||
builder.actionBuilder = (context, state) => BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
|
@ -31,10 +31,12 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
popupBuilder: (context) => _buildEmojiPicker(),
|
||||
child: FlowyTextButton(
|
||||
emoji,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: emojiSize,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 35.0),
|
||||
fillColor: Colors.transparent,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onPressed: () {
|
||||
popoverController.show();
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../base/emoji_picker_button.dart';
|
||||
@ -147,7 +148,9 @@ class _CalloutBlockComponentWidgetState
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: EmojiPickerButton(
|
||||
key: ValueKey(emoji), // force to refresh the popover state
|
||||
key: ValueKey(
|
||||
emoji.toString(),
|
||||
), // force to refresh the popover state
|
||||
emoji: emoji,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji.emoji);
|
||||
@ -157,13 +160,11 @@ class _CalloutBlockComponentWidgetState
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: buildCalloutBlockComponent(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10.0,
|
||||
)
|
||||
const VSpace(10),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -150,7 +150,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
fontColor: Theme.of(context).colorScheme.tertiary,
|
||||
onPressed: () async {
|
||||
final hasFileImageCover = CoverSelectionType.fromString(
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
) ==
|
||||
CoverSelectionType.file;
|
||||
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
||||
@ -220,9 +220,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
pickerBackgroundColor: theme.cardColor,
|
||||
pickerItemHoverColor: theme.hoverColor,
|
||||
selectedBackgroundColorHex:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute] ==
|
||||
widget.node.attributes[CoverBlockKeys.selectionType] ==
|
||||
CoverSelectionType.color.toString()
|
||||
? widget.node.attributes[kCoverSelectionAttribute]
|
||||
? widget.node.attributes[CoverBlockKeys.selection]
|
||||
: 'ffffff',
|
||||
backgroundColorOptions:
|
||||
_generateBackgroundColorOptions(widget.editorState),
|
||||
@ -284,7 +284,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
final changeCoverBloc =
|
||||
context.read<ChangeCoverPopoverBloc>();
|
||||
final deletingCurrentCover =
|
||||
widget.node.attributes[kCoverSelectionAttribute] ==
|
||||
widget.node.attributes[CoverBlockKeys.selection] ==
|
||||
images[index - 1];
|
||||
if (deletingCurrentCover) {
|
||||
await showDialog(
|
||||
|
@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
|
||||
deleteImage: (DeleteImage deleteImage) async {
|
||||
final currentState = state;
|
||||
final currentlySelectedImage =
|
||||
node.attributes[kCoverSelectionAttribute];
|
||||
node.attributes[CoverBlockKeys.selection];
|
||||
if (currentState is Loaded) {
|
||||
await _deleteImageInStorage(deleteImage.path);
|
||||
if (currentlySelectedImage == deleteImage.path) {
|
||||
@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
|
||||
clearAllImages: (ClearAllImages clearAllImages) async {
|
||||
final currentState = state;
|
||||
final currentlySelectedImage =
|
||||
node.attributes[kCoverSelectionAttribute];
|
||||
node.attributes[CoverBlockKeys.selection];
|
||||
|
||||
if (currentState is Loaded) {
|
||||
for (final image in currentState.imageNames) {
|
||||
@ -90,8 +90,9 @@ class ChangeCoverPopoverBloc
|
||||
Future<void> _removeCoverImageFromNode() async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(node, {
|
||||
kCoverSelectionTypeAttribute: CoverSelectionType.initial.toString(),
|
||||
kIconSelectionAttribute: node.attributes[kIconSelectionAttribute]
|
||||
CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
|
||||
CoverBlockKeys.iconSelection:
|
||||
node.attributes[CoverBlockKeys.iconSelection]
|
||||
});
|
||||
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:flutter/material.dart';
|
||||
|
||||
const String kCoverType = 'cover';
|
||||
const String kCoverSelectionTypeAttribute = 'cover_selection_type';
|
||||
const String kCoverSelectionAttribute = 'cover_selection';
|
||||
const String kIconSelectionAttribute = 'selected_icon';
|
||||
class CoverBlockKeys {
|
||||
const CoverBlockKeys._();
|
||||
|
||||
static const String selectionType = 'cover_selection_type';
|
||||
static const String selection = 'cover_selection';
|
||||
static const String iconSelection = 'selected_icon';
|
||||
}
|
||||
|
||||
enum CoverSelectionType {
|
||||
initial,
|
||||
@ -69,7 +72,7 @@ class CoverImageNodeWidget extends StatefulWidget {
|
||||
|
||||
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
||||
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
);
|
||||
|
||||
@override
|
||||
@ -105,9 +108,10 @@ class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
||||
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kCoverSelectionTypeAttribute: type.toString(),
|
||||
kCoverSelectionAttribute: cover,
|
||||
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
||||
CoverBlockKeys.selectionType: type.toString(),
|
||||
CoverBlockKeys.selection: cover,
|
||||
CoverBlockKeys.iconSelection:
|
||||
widget.node.attributes[CoverBlockKeys.iconSelection]
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -247,11 +251,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
||||
Future<void> _insertIcon(Emoji emoji) async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kCoverSelectionTypeAttribute:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
kCoverSelectionAttribute:
|
||||
widget.node.attributes[kCoverSelectionAttribute],
|
||||
kIconSelectionAttribute: emoji.emoji,
|
||||
CoverBlockKeys.selectionType:
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
CoverBlockKeys.selection:
|
||||
widget.node.attributes[CoverBlockKeys.selection],
|
||||
CoverBlockKeys.iconSelection: emoji.emoji,
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -259,11 +263,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
||||
Future<void> _removeIcon() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kIconSelectionAttribute: "",
|
||||
kCoverSelectionTypeAttribute:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
kCoverSelectionAttribute:
|
||||
widget.node.attributes[kCoverSelectionAttribute],
|
||||
CoverBlockKeys.iconSelection: "",
|
||||
CoverBlockKeys.selectionType:
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
CoverBlockKeys.selection:
|
||||
widget.node.attributes[CoverBlockKeys.selection],
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -297,15 +301,16 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
final popoverController = PopoverController();
|
||||
|
||||
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
);
|
||||
Color get color => Color(
|
||||
int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
|
||||
int.tryParse(widget.node.attributes[CoverBlockKeys.selection]) ??
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
|
||||
? false
|
||||
: widget.node.attributes[kIconSelectionAttribute].isNotEmpty;
|
||||
bool get hasIcon =>
|
||||
widget.node.attributes[CoverBlockKeys.iconSelection] == null
|
||||
? false
|
||||
: widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty;
|
||||
bool isOverlayButtonsHidden = true;
|
||||
PopoverController iconPopoverController = PopoverController();
|
||||
bool get hasCover =>
|
||||
@ -336,7 +341,7 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
constraints: BoxConstraints.loose(const Size(320, 380)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: EmojiIconWidget(
|
||||
emoji: widget.node.attributes[kIconSelectionAttribute],
|
||||
emoji: widget.node.attributes[CoverBlockKeys.iconSelection],
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return EmojiPopover(
|
||||
@ -375,9 +380,10 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kCoverSelectionTypeAttribute: type.toString(),
|
||||
kCoverSelectionAttribute: cover,
|
||||
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
||||
CoverBlockKeys.selectionType: type.toString(),
|
||||
CoverBlockKeys.selection: cover,
|
||||
CoverBlockKeys.iconSelection:
|
||||
widget.node.attributes[CoverBlockKeys.iconSelection]
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -385,11 +391,11 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
Future<void> _insertIcon(Emoji emoji) async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kCoverSelectionTypeAttribute:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
kCoverSelectionAttribute:
|
||||
widget.node.attributes[kCoverSelectionAttribute],
|
||||
kIconSelectionAttribute: emoji.emoji,
|
||||
CoverBlockKeys.selectionType:
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
CoverBlockKeys.selection:
|
||||
widget.node.attributes[CoverBlockKeys.selection],
|
||||
CoverBlockKeys.iconSelection: emoji.emoji,
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -397,11 +403,11 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
Future<void> _removeIcon() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
kIconSelectionAttribute: "",
|
||||
kCoverSelectionTypeAttribute:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
kCoverSelectionAttribute:
|
||||
widget.node.attributes[kCoverSelectionAttribute],
|
||||
CoverBlockKeys.iconSelection: "",
|
||||
CoverBlockKeys.selectionType:
|
||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
||||
CoverBlockKeys.selection:
|
||||
widget.node.attributes[CoverBlockKeys.selection],
|
||||
});
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
@ -409,7 +415,7 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
Widget _buildCoverOverlayButtons(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 20,
|
||||
right: 260,
|
||||
right: EditorStyleCustomizer.horizontalPadding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -480,7 +486,7 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
switch (selectionType) {
|
||||
case CoverSelectionType.file:
|
||||
final imageFile =
|
||||
File(widget.node.attributes[kCoverSelectionAttribute]);
|
||||
File(widget.node.attributes[CoverBlockKeys.selection]);
|
||||
if (!imageFile.existsSync()) {
|
||||
// reset cover state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -496,7 +502,7 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
break;
|
||||
case CoverSelectionType.asset:
|
||||
coverImage = Image.asset(
|
||||
widget.node.attributes[kCoverSelectionAttribute],
|
||||
widget.node.attributes[CoverBlockKeys.selection],
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
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 prompt = 'prompt';
|
||||
static const String startSelection = 'start_selection';
|
||||
static const String generationCount = 'generation_count';
|
||||
}
|
||||
|
||||
Node autoCompletionNode({
|
||||
@ -35,6 +36,7 @@ Node autoCompletionNode({
|
||||
attributes: {
|
||||
AutoCompletionBlockKeys.prompt: prompt,
|
||||
AutoCompletionBlockKeys.startSelection: start.toJson(),
|
||||
AutoCompletionBlockKeys.generationCount: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -92,6 +94,16 @@ class _AutoCompletionBlockComponentState
|
||||
late final SelectionGestureInterceptor interceptor;
|
||||
|
||||
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
|
||||
void initState() {
|
||||
@ -106,6 +118,7 @@ class _AutoCompletionBlockComponentState
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_onExit();
|
||||
_unsubscribeSelectionGesture();
|
||||
controller.dispose();
|
||||
|
||||
@ -124,7 +137,7 @@ class _AutoCompletionBlockComponentState
|
||||
children: [
|
||||
const AutoCompletionHeader(),
|
||||
const Space(0, 10),
|
||||
if (prompt.isEmpty) ...[
|
||||
if (prompt.isEmpty && generationCount < 1) ...[
|
||||
_buildInputWidget(context),
|
||||
const Space(0, 10),
|
||||
AutoCompletionInputFooter(
|
||||
@ -134,6 +147,7 @@ class _AutoCompletionBlockComponentState
|
||||
] else ...[
|
||||
AutoCompletionFooter(
|
||||
onKeep: _onExit,
|
||||
onRewrite: _onRewrite,
|
||||
onDiscard: _onDiscard,
|
||||
)
|
||||
],
|
||||
@ -213,13 +227,39 @@ class _AutoCompletionBlockComponentState
|
||||
await _showError(error.message);
|
||||
},
|
||||
);
|
||||
await _updateGenerationCount();
|
||||
}
|
||||
|
||||
Future<void> _onDiscard() async {
|
||||
final selection =
|
||||
widget.node.attributes[AutoCompletionBlockKeys.startSelection];
|
||||
final selection = startSelection;
|
||||
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;
|
||||
if (end != null) {
|
||||
final transaction = editorState.transaction;
|
||||
@ -230,7 +270,71 @@ class _AutoCompletionBlockComponentState
|
||||
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 {
|
||||
@ -244,6 +348,21 @@ class _AutoCompletionBlockComponentState
|
||||
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 {
|
||||
// make sure the previous node is a empty paragraph node without any styles.
|
||||
final transaction = editorState.transaction;
|
||||
@ -393,10 +512,12 @@ class AutoCompletionFooter extends StatelessWidget {
|
||||
const AutoCompletionFooter({
|
||||
super.key,
|
||||
required this.onKeep,
|
||||
required this.onRewrite,
|
||||
required this.onDiscard,
|
||||
});
|
||||
|
||||
final VoidCallback onKeep;
|
||||
final VoidCallback onRewrite;
|
||||
final VoidCallback onDiscard;
|
||||
|
||||
@override
|
||||
@ -408,6 +529,11 @@ class AutoCompletionFooter extends StatelessWidget {
|
||||
onPressed: onKeep,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||
onPressed: onRewrite,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_discard.tr(),
|
||||
onPressed: onDiscard,
|
||||
|
@ -15,10 +15,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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 {
|
||||
const SmartEditBlockKeys._();
|
||||
|
||||
|
@ -11,7 +11,7 @@ class EditorStyleCustomizer {
|
||||
});
|
||||
|
||||
static double get horizontalPadding =>
|
||||
PlatformExtension.isDesktop ? 100.0 : 10.0;
|
||||
PlatformExtension.isDesktop ? 50.0 : 10.0;
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
@ -28,21 +28,18 @@ class EditorStyleCustomizer {
|
||||
final theme = Theme.of(context);
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
return EditorStyle.desktop(
|
||||
padding: EdgeInsets.only(
|
||||
left: horizontalPadding / 2.0,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
cursorColor: theme.colorScheme.primary,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
text: TextStyle(
|
||||
fontFamily: 'poppins',
|
||||
fontFamily: 'Poppins',
|
||||
fontSize: fontSize,
|
||||
color: theme.colorScheme.onBackground,
|
||||
height: 1.5,
|
||||
),
|
||||
bold: const TextStyle(
|
||||
fontFamily: 'poppins-Bold',
|
||||
fontFamily: 'Poppins-Bold',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
@ -15,24 +16,20 @@ class ExportPageWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowyText.regular(
|
||||
'There are some errors.',
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
const FlowyText.medium(
|
||||
'Open document failed',
|
||||
fontSize: 18.0,
|
||||
),
|
||||
const VSpace(5),
|
||||
const FlowyText.regular(
|
||||
'Please try to export the page and contact us.',
|
||||
fontSize: 14.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
FlowyTextButton(
|
||||
'Export page',
|
||||
constraints: const BoxConstraints(maxWidth: 100),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
const VSpace(20),
|
||||
RoundedTextButton(
|
||||
title: 'Export page',
|
||||
width: 100,
|
||||
height: 30,
|
||||
onPressed: onTap,
|
||||
)
|
||||
],
|
||||
|
@ -53,8 +53,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: b1a1b14
|
||||
resolved-ref: b1a1b14f35114a7becdb3e2de909d546d7328a59
|
||||
ref: ead61af
|
||||
resolved-ref: ead61afb796037e8ceb63ba4bcf439818514ed4b
|
||||
url: "https://github.com/LucasXu0/appflowy-editor.git"
|
||||
source: git
|
||||
version: "0.1.12"
|
||||
|
@ -47,7 +47,7 @@ dependencies:
|
||||
# path: /Users/lucas.xu/Desktop/appflowy-editor
|
||||
git:
|
||||
url: https://github.com/LucasXu0/appflowy-editor.git
|
||||
ref: b1a1b14
|
||||
ref: ead61af
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
@ -179,6 +179,7 @@ flutter:
|
||||
- assets/images/login/
|
||||
- assets/images/grid/setting/
|
||||
- assets/translations/
|
||||
- assets/template/readme.json
|
||||
|
||||
# The following assets will be excluded in release.
|
||||
# BEGIN: EXCLUDE_IN_RELEASE
|
||||
|
@ -1,49 +1,49 @@
|
||||
import {
|
||||
FlowyError,
|
||||
DocumentDataPB2,
|
||||
OpenDocumentPayloadPBV2,
|
||||
CreateDocumentPayloadPBV2,
|
||||
ApplyActionPayloadPBV2,
|
||||
DocumentDataPB,
|
||||
OpenDocumentPayloadPB,
|
||||
CreateDocumentPayloadPB,
|
||||
ApplyActionPayloadPB,
|
||||
BlockActionPB,
|
||||
CloseDocumentPayloadPBV2,
|
||||
CloseDocumentPayloadPB,
|
||||
} from '@/services/backend';
|
||||
import { Result } from 'ts-results';
|
||||
import {
|
||||
DocumentEvent2ApplyAction,
|
||||
DocumentEvent2CloseDocument,
|
||||
DocumentEvent2OpenDocument,
|
||||
DocumentEvent2CreateDocument,
|
||||
DocumentEventApplyAction,
|
||||
DocumentEventCloseDocument,
|
||||
DocumentEventOpenDocument,
|
||||
DocumentEventCreateDocument,
|
||||
} from '@/services/backend/events/flowy-document2';
|
||||
|
||||
export class DocumentBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
create = (): Promise<Result<void, FlowyError>> => {
|
||||
const payload = CreateDocumentPayloadPBV2.fromObject({
|
||||
const payload = CreateDocumentPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEvent2CreateDocument(payload);
|
||||
return DocumentEventCreateDocument(payload);
|
||||
};
|
||||
|
||||
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPBV2.fromObject({
|
||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEvent2OpenDocument(payload);
|
||||
return DocumentEventOpenDocument(payload);
|
||||
};
|
||||
|
||||
applyActions = (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]): Promise<Result<void, FlowyError>> => {
|
||||
const payload = ApplyActionPayloadPBV2.fromObject({
|
||||
const payload = ApplyActionPayloadPB.fromObject({
|
||||
document_id: this.viewId,
|
||||
actions: actions,
|
||||
});
|
||||
return DocumentEvent2ApplyAction(payload);
|
||||
return DocumentEventApplyAction(payload);
|
||||
};
|
||||
|
||||
close = (): Promise<Result<void, FlowyError>> => {
|
||||
const payload = CloseDocumentPayloadPBV2.fromObject({
|
||||
const payload = CloseDocumentPayloadPB.fromObject({
|
||||
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"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"appflowy-integrate",
|
||||
"bytes",
|
||||
"collab",
|
||||
|
@ -25,6 +25,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = {version = "1.0"}
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.4.0"
|
||||
|
@ -3,12 +3,12 @@ use std::{collections::HashMap, vec};
|
||||
use collab_document::blocks::{Block, DocumentData, DocumentMeta};
|
||||
use nanoid::nanoid;
|
||||
|
||||
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
|
||||
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentDataWrapper(pub DocumentData);
|
||||
|
||||
impl From<DocumentDataWrapper> for DocumentDataPB2 {
|
||||
impl From<DocumentDataWrapper> for DocumentDataPB {
|
||||
fn from(data: DocumentDataWrapper) -> Self {
|
||||
let blocks = data
|
||||
.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
|
||||
impl Default for DocumentDataWrapper {
|
||||
fn default() -> Self {
|
||||
|
@ -3,28 +3,28 @@ use std::collections::HashMap;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct OpenDocumentPayloadPBV2 {
|
||||
pub struct OpenDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
// Support customize initial data
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct CreateDocumentPayloadPBV2 {
|
||||
pub struct CreateDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
// Support customize initial data
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub initial_data: Option<DocumentDataPB>,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct CloseDocumentPayloadPBV2 {
|
||||
pub struct CloseDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
// Support customize initial data
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct ApplyActionPayloadPBV2 {
|
||||
pub struct ApplyActionPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
|
||||
@ -33,14 +33,14 @@ pub struct ApplyActionPayloadPBV2 {
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct GetDocumentDataPayloadPBV2 {
|
||||
pub struct GetDocumentDataPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
// Support customize initial data
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct DocumentDataPB2 {
|
||||
pub struct DocumentDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub page_id: String,
|
||||
|
||||
|
@ -17,38 +17,40 @@ use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataRes
|
||||
use crate::{
|
||||
document_data::DocumentDataWrapper,
|
||||
entities::{
|
||||
ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
||||
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPBV2, CreateDocumentPayloadPBV2, DeltaTypePB,
|
||||
DocEventPB, DocumentDataPB2, OpenDocumentPayloadPBV2,
|
||||
ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
|
||||
BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, CreateDocumentPayloadPB, DeltaTypePB,
|
||||
DocEventPB, DocumentDataPB, OpenDocumentPayloadPB,
|
||||
},
|
||||
manager::DocumentManager,
|
||||
};
|
||||
|
||||
// Handler for creating a new document
|
||||
pub(crate) async fn create_document_handler(
|
||||
data: AFPluginData<CreateDocumentPayloadPBV2>,
|
||||
data: AFPluginData<CreateDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let context = data.into_inner();
|
||||
// Create a new document with a default content, one page block and one text block
|
||||
let data = DocumentDataWrapper::default();
|
||||
manager.create_document(context.document_id, data)?;
|
||||
let initial_data: DocumentDataWrapper = context
|
||||
.initial_data
|
||||
.map(|data| data.into())
|
||||
.unwrap_or_default();
|
||||
manager.create_document(context.document_id, initial_data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Handler for opening an existing document
|
||||
pub(crate) async fn open_document_handler(
|
||||
data: AFPluginData<OpenDocumentPayloadPBV2>,
|
||||
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentDataPB2, FlowyError> {
|
||||
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||
let context = data.into_inner();
|
||||
let document = manager.open_document(context.document_id)?;
|
||||
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(
|
||||
data: AFPluginData<CloseDocumentPayloadPBV2>,
|
||||
data: AFPluginData<CloseDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let context = data.into_inner();
|
||||
@ -59,18 +61,18 @@ pub(crate) async fn close_document_handler(
|
||||
// Get the content of the existing document,
|
||||
// if the document does not exist, return an error.
|
||||
pub(crate) async fn get_document_data_handler(
|
||||
data: AFPluginData<OpenDocumentPayloadPBV2>,
|
||||
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentDataPB2, FlowyError> {
|
||||
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||
let context = data.into_inner();
|
||||
let document = manager.get_document(context.document_id)?;
|
||||
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
|
||||
pub(crate) async fn apply_action_handler(
|
||||
data: AFPluginData<ApplyActionPayloadPBV2>,
|
||||
data: AFPluginData<ApplyActionPayloadPB>,
|
||||
manager: AFPluginState<Arc<DocumentManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let context = data.into_inner();
|
||||
|
@ -17,30 +17,30 @@ pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
|
||||
.name(env!("CARGO_PKG_NAME"))
|
||||
.state(document_manager);
|
||||
|
||||
plugin = plugin.event(DocumentEvent2::OpenDocument, open_document_handler);
|
||||
plugin = plugin.event(DocumentEvent2::CloseDocument, close_document_handler);
|
||||
plugin = plugin.event(DocumentEvent2::ApplyAction, apply_action_handler);
|
||||
plugin = plugin.event(DocumentEvent2::CreateDocument, create_document_handler);
|
||||
plugin = plugin.event(DocumentEvent2::GetDocumentData, get_document_data_handler);
|
||||
plugin = plugin.event(DocumentEvent::CreateDocument, create_document_handler);
|
||||
plugin = plugin.event(DocumentEvent::OpenDocument, open_document_handler);
|
||||
plugin = plugin.event(DocumentEvent::CloseDocument, close_document_handler);
|
||||
plugin = plugin.event(DocumentEvent::ApplyAction, apply_action_handler);
|
||||
plugin = plugin.event(DocumentEvent::GetDocumentData, get_document_data_handler);
|
||||
|
||||
plugin
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum DocumentEvent2 {
|
||||
#[event(input = "CreateDocumentPayloadPBV2")]
|
||||
pub enum DocumentEvent {
|
||||
#[event(input = "CreateDocumentPayloadPB")]
|
||||
CreateDocument = 0,
|
||||
|
||||
#[event(input = "OpenDocumentPayloadPBV2", output = "DocumentDataPB2")]
|
||||
#[event(input = "OpenDocumentPayloadPB", output = "DocumentDataPB")]
|
||||
OpenDocument = 1,
|
||||
|
||||
#[event(input = "CloseDocumentPayloadPBV2")]
|
||||
#[event(input = "CloseDocumentPayloadPB")]
|
||||
CloseDocument = 2,
|
||||
|
||||
#[event(input = "ApplyActionPayloadPBV2")]
|
||||
#[event(input = "ApplyActionPayloadPB")]
|
||||
ApplyAction = 3,
|
||||
|
||||
#[event(input = "GetDocumentDataPayloadPBV2")]
|
||||
#[event(input = "GetDocumentDataPayloadPB")]
|
||||
GetDocumentData = 4,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user