feat: load the readme file when first time launch (#2676)
* chore: bump version 0.2.0 * test: add migration test * feat: support exporting document data to afdoc format (#2610) * feat: support exporting document data to afdoc format * feat: export database * fix: resolve comment issues * fix: add error tips when exporting files failed --------- Co-authored-by: nathan <nathan@appflowy.io> * feat: rename delta * feat: add built-in readme * fix: use independent children id * fix: comment typo * chore: add icons * fix: floating toolbar style * fix: markdown export test * chore: disbale moveup/movedown action * fix: unit test --------- Co-authored-by: nathan <nathan@appflowy.io>
@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.1.5"
|
||||
CURRENT_APP_VERSION = "0.2.0"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
@ -1 +1,4 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="plus" style="width: 16px; height: 100%; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="16" height="16" ><path d="M7.977 14.963c.407 0 .747-.324.747-.723V8.72h5.362c.399 0 .74-.34.74-.747a.746.746 0 00-.74-.738H8.724V1.706c0-.398-.34-.722-.747-.722a.732.732 0 00-.739.722v5.529h-5.37a.746.746 0 00-.74.738c0 .407.341.747.74.747h5.37v5.52c0 .399.332.723.739.723z" fill-opacity="0.35" fill="#37352F"></path></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="7.5" y="4" width="1" height="8" rx="0.5" fill="#333333"/>
|
||||
<rect x="12" y="7.5" width="1" height="8" rx="0.5" transform="rotate(90 12 7.5)" fill="#333333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 558 B After Width: | Height: | Size: 268 B |
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2990 5100 c-19 -10 -382 -366 -806 -792 l-771 -773 -42 -85 c-125
|
||||
-256 -84 -501 120 -714 47 -50 91 -101 98 -113 14 -27 14 -116 0 -168 -57
|
||||
-207 -310 -495 -535 -606 -73 -37 -92 -42 -190 -50 -60 -4 -145 -18 -189 -29
|
||||
-312 -80 -560 -328 -646 -645 -32 -116 -32 -336 -1 -450 88 -318 330 -560 647
|
||||
-646 115 -32 335 -32 450 0 263 71 474 247 591 494 45 95 71 203 83 337 7 90
|
||||
15 122 40 176 149 318 583 627 777 554 15 -6 68 -50 119 -98 214 -205 459
|
||||
-246 715 -121 85 42 85 42 849 803 420 418 776 781 792 805 34 53 37 97 10
|
||||
149 -24 46 -1934 1954 -1978 1976 -42 21 -90 20 -133 -4z m920 -1195 l845
|
||||
-845 -318 -317 -317 -318 -847 847 -848 848 315 315 c173 173 317 315 320 315
|
||||
3 0 385 -380 850 -845z m-282 -1972 c-320 -320 -330 -327 -458 -328 -100 0
|
||||
-145 23 -264 135 -52 49 -114 99 -137 111 -242 123 -589 11 -902 -291 -135
|
||||
-130 -224 -245 -287 -375 -54 -110 -80 -200 -80 -285 -2 -277 -206 -528 -475
|
||||
-585 -203 -43 -395 15 -545 164 -190 188 -233 466 -108 704 42 81 164 203 245
|
||||
245 77 41 198 72 277 72 326 1 780 374 949 780 74 178 76 355 5 494 -11 21
|
||||
-59 80 -108 132 -110 117 -135 164 -135 259 0 131 3 136 324 458 l286 287 847
|
||||
-847 848 -848 -282 -282z"/>
|
||||
<path d="M828 1029 c-43 -22 -78 -81 -78 -129 0 -76 74 -150 150 -150 17 0 49
|
||||
9 70 20 45 23 80 80 80 130 0 50 -35 107 -80 130 -49 25 -94 25 -142 -1z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,6 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4.3999H4.11111H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.77775 4.4V3.2C5.77775 2.88174 5.89481 2.57652 6.10319 2.35147C6.31156 2.12643 6.59418 2 6.88886 2H9.11108C9.40577 2 9.68838 2.12643 9.89676 2.35147C10.1051 2.57652 10.2222 2.88174 10.2222 3.2V4.4M11.8889 4.4V12.8C11.8889 13.1183 11.7718 13.4235 11.5634 13.6485C11.3551 13.8736 11.0724 14 10.7778 14H5.2222C4.92751 14 4.64489 13.8736 4.43652 13.6485C4.22815 13.4235 4.11108 13.1183 4.11108 12.8V4.4H11.8889Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.88892 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.11108 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 4.40039H4.11111H13" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.77799 4.4V3.2C5.77799 2.88174 5.89506 2.57652 6.10343 2.35147C6.31181 2.12643 6.59442 2 6.88911 2H9.11133C9.40601 2 9.68863 2.12643 9.897 2.35147C10.1054 2.57652 10.2224 2.88174 10.2224 3.2V4.4M11.8891 4.4V12.8C11.8891 13.1183 11.772 13.4235 11.5637 13.6485C11.3553 13.8736 11.0727 14 10.778 14H5.22244C4.92775 14 4.64514 13.8736 4.43676 13.6485C4.22839 13.4235 4.11133 13.1183 4.11133 12.8V4.4H11.8891Z" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.88867 7.40039V11.0004" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.11133 7.40039V11.0004" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 886 B After Width: | Height: | Size: 886 B |
@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.974 6.33301H7.35865C6.7922 6.33301 6.33301 6.7922 6.33301 7.35865V11.974C6.33301 12.5405 6.7922 12.9997 7.35865 12.9997H11.974C12.5405 12.9997 12.9997 12.5405 12.9997 11.974V7.35865C12.9997 6.7922 12.5405 6.33301 11.974 6.33301Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.974 6.33301H7.35865C6.7922 6.33301 6.33301 6.7922 6.33301 7.35865V11.974C6.33301 12.5405 6.7922 12.9997 7.35865 12.9997H11.974C12.5405 12.9997 12.9997 12.5405 12.9997 11.974V7.35865C12.9997 6.7922 12.5405 6.33301 11.974 6.33301Z" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 785 B |
@ -1 +1,8 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" class="dragHandle" style="width: 14px; height: 14px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="14" height="14" ><path d="M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z" fill-opacity="0.35" fill="#37352F"></path></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="9" y="3" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
<rect x="5" y="3" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
<rect x="9" y="7" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
<rect x="5" y="7" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
<rect x="9" y="11" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
<rect x="5" y="11" width="2" height="2" rx="0.5" fill="#333333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 495 B |
@ -187,6 +187,7 @@
|
||||
"files": {
|
||||
"copy": "Copy",
|
||||
"defaultLocation": "Read files and data storage location",
|
||||
"exportData": "Export your data",
|
||||
"doubleTapToCopy": "Double tap to copy the path",
|
||||
"restoreLocation": "Restore to AppFlowy default path",
|
||||
"customizeLocation": "Open another folder",
|
||||
@ -209,7 +210,9 @@
|
||||
"changeLocationTooltips": "Change the files read the data directory",
|
||||
"change": "Change",
|
||||
"openLocationTooltips": "Open the files read the data directory",
|
||||
"recoverLocationTooltips": "Recover the files read the data directory"
|
||||
"recoverLocationTooltips": "Recover the files read the data directory",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!"
|
||||
},
|
||||
"user": {
|
||||
"name": "Name",
|
||||
|
@ -91,7 +91,6 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
class _BackendKeys {
|
||||
const _BackendKeys._();
|
||||
|
||||
static const String page = 'page';
|
||||
static const String text = 'text';
|
||||
}
|
||||
|
||||
@ -109,7 +108,6 @@ extension BlockToNode on BlockPB {
|
||||
|
||||
String _typeAdapter(String ty) {
|
||||
final adapter = {
|
||||
_BackendKeys.page: 'document',
|
||||
_BackendKeys.text: ParagraphBlockKeys.type,
|
||||
};
|
||||
return adapter[ty] ?? ty;
|
||||
@ -149,7 +147,6 @@ extension NodeToBlock on Node {
|
||||
|
||||
String _typeAdapter(String type) {
|
||||
final adapter = {
|
||||
'document': 'page',
|
||||
'paragraph': 'text',
|
||||
};
|
||||
return adapter[type] ?? type;
|
||||
|
@ -114,6 +114,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
child: FloatingToolbar(
|
||||
style: styleCustomizer.floatingToolbarStyleBuilder(),
|
||||
items: toolbarItems,
|
||||
editorState: widget.editorState,
|
||||
scrollController: scrollController,
|
||||
@ -127,16 +128,16 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
OptionAction.divider,
|
||||
OptionAction.moveUp,
|
||||
OptionAction.moveDown,
|
||||
// OptionAction.divider,
|
||||
// OptionAction.moveUp,
|
||||
// OptionAction.moveDown,
|
||||
];
|
||||
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 4.0),
|
||||
);
|
||||
final customBlockComponentBuilderMap = {
|
||||
'document': DocumentComponentBuilder(),
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
ParagraphBlockKeys.type: TextBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
@ -207,7 +208,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == 'document') {
|
||||
if (entry.key == PageBlockKeys.type) {
|
||||
continue;
|
||||
}
|
||||
final builder = entry.value;
|
||||
@ -224,7 +225,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
];
|
||||
|
||||
final colorAction = [
|
||||
OptionAction.divider,
|
||||
// OptionAction.divider,
|
||||
OptionAction.color,
|
||||
];
|
||||
|
||||
|
@ -29,7 +29,7 @@ class BlockActionButton extends StatelessWidget {
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: svgWidget(
|
||||
svgName,
|
||||
size: const Size.square(14.0),
|
||||
size: const Size.square(18.0),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
|
@ -120,9 +120,6 @@ class BlockOptionButton extends StatelessWidget {
|
||||
transaction.moveNode(node.path.next.next, node);
|
||||
break;
|
||||
case OptionAction.color:
|
||||
// show the color picker
|
||||
|
||||
break;
|
||||
case OptionAction.divider:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
enum OptionAction {
|
||||
delete,
|
||||
@ -36,10 +37,10 @@ class ColorOptionAction extends PopoverActionCell {
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) {
|
||||
return svgWidget(
|
||||
'editor/color_formatter',
|
||||
color: iconColor,
|
||||
);
|
||||
return const FlowySvg(
|
||||
name: 'editor/color_formatter',
|
||||
size: Size.square(12),
|
||||
).padding(all: 2.0);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -125,18 +126,13 @@ class OptionActionWrapper extends ActionCell {
|
||||
case OptionAction.moveDown:
|
||||
name = 'editor/move_down';
|
||||
break;
|
||||
case OptionAction.color:
|
||||
throw UnimplementedError();
|
||||
case OptionAction.divider:
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
if (name.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return svgWidget(
|
||||
name,
|
||||
color: iconColor,
|
||||
);
|
||||
return FlowySvg(name: name);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -103,9 +103,6 @@ class OptionActionList extends StatelessWidget {
|
||||
transaction.moveNode(node.path.next.next, node);
|
||||
break;
|
||||
case OptionAction.color:
|
||||
// show the color picker
|
||||
|
||||
break;
|
||||
case OptionAction.divider:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -37,12 +37,12 @@ class EditorMigration {
|
||||
(element) => element.id == 'cover',
|
||||
);
|
||||
if (coverNode != null) {
|
||||
node = documentNode(
|
||||
node = pageNode(
|
||||
children: children,
|
||||
attributes: coverNode.attributes,
|
||||
);
|
||||
} else {
|
||||
node = documentNode(children: children);
|
||||
node = pageNode(children: children);
|
||||
}
|
||||
} else if (id == 'callout') {
|
||||
final emoji = nodeV0.attributes['emoji'] ?? '📌';
|
||||
@ -141,12 +141,13 @@ class EditorMigration {
|
||||
}
|
||||
const backgroundColor = 'backgroundColor';
|
||||
if (attributes.containsKey(backgroundColor)) {
|
||||
attributes['highlightColor'] = attributes[backgroundColor];
|
||||
attributes[FlowyRichTextKeys.highlightColor] =
|
||||
attributes[backgroundColor];
|
||||
attributes.remove(backgroundColor);
|
||||
}
|
||||
const color = 'color';
|
||||
if (attributes.containsKey(color)) {
|
||||
attributes['textColor'] = attributes[color];
|
||||
attributes[FlowyRichTextKeys.textColor] = attributes[color];
|
||||
attributes.remove(color);
|
||||
}
|
||||
return attributes;
|
||||
|
@ -375,7 +375,7 @@ class _AutoCompletionBlockComponentState
|
||||
final previous = widget.node.previous;
|
||||
final Selection selection;
|
||||
if (previous == null ||
|
||||
previous.type != 'paragraph' ||
|
||||
previous.type != ParagraphBlockKeys.type ||
|
||||
(previous.delta?.toPlainText().isNotEmpty ?? false)) {
|
||||
selection = Selection.single(
|
||||
path: widget.node.path,
|
||||
|
@ -136,4 +136,11 @@ class EditorStyleCustomizer {
|
||||
selectionMenuItemSelectedColor: theme.hoverColor,
|
||||
);
|
||||
}
|
||||
|
||||
FloatingToolbarStyle floatingToolbarStyleBuilder() {
|
||||
final theme = Theme.of(context);
|
||||
return FloatingToolbarStyle(
|
||||
backgroundColor: theme.cardColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,20 @@ class SettingsFileExportState {
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<ViewPB> get selectedViews {
|
||||
final selectedViews = <ViewPB>[];
|
||||
for (var i = 0; i < views.length; i++) {
|
||||
if (selectedApps[i]) {
|
||||
for (var j = 0; j < views[i].childViews.length; j++) {
|
||||
if (selectedItems[i][j]) {
|
||||
selectedViews.add(views[i].childViews[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedViews;
|
||||
}
|
||||
|
||||
List<ViewPB> views;
|
||||
List<bool> expanded = [];
|
||||
List<bool> selectedApps = [];
|
||||
|
@ -0,0 +1,73 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class SettingsExportFileWidget extends StatefulWidget {
|
||||
const SettingsExportFileWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsExportFileWidget> createState() =>
|
||||
SettingsExportFileWidgetState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_exportData.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
_OpenExportedDirectoryButton(
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const FlowyDialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
child: FileExporterWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OpenExportedDirectoryButton extends StatelessWidget {
|
||||
const _OpenExportedDirectoryButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
tooltipText: LocaleKeys.settings_files_open.tr(),
|
||||
icon: svgWidget(
|
||||
'common/open_folder',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: onTap,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,15 +1,22 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class FileExporterWidget extends StatefulWidget {
|
||||
@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget {
|
||||
class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
// Map<String, List<String>> _selectedPages = {};
|
||||
|
||||
SettingsFileExporterCubit? cubit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(child: _buildFileSelector(context)),
|
||||
const VSpace(8),
|
||||
_buildButtons(context)
|
||||
],
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final views = workspaces.workspace.views;
|
||||
cubit ??= SettingsFileExporterCubit(views: views);
|
||||
return BlocProvider<SettingsFileExporterCubit>.value(
|
||||
value: cubit!,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Expanded(child: _ExpandedList()),
|
||||
const VSpace(8),
|
||||
_buildButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildButtons(BuildContext context) {
|
||||
Widget _buildButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
onPressed: () async {
|
||||
await getIt<FilePickerService>()
|
||||
.getDirectoryPath()
|
||||
.then((exportPath) {
|
||||
Navigator.of(context).pop();
|
||||
.then((exportPath) async {
|
||||
if (exportPath != null && cubit != null) {
|
||||
final views = cubit!.state.selectedViews;
|
||||
final result =
|
||||
await _AppFlowyFileExporter.exportToPath(exportPath, views);
|
||||
if (result.$1) {
|
||||
// success
|
||||
_showToast(LocaleKeys.settings_files_exportFileSuccess.tr());
|
||||
} else {
|
||||
_showToast(
|
||||
LocaleKeys.settings_files_exportFileFail.tr() +
|
||||
result.$2.join('\n'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showToast(LocaleKeys.settings_files_exportFileFail.tr());
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).popUntil(
|
||||
(router) => router.settings.name == '/',
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
|
||||
_buildFileSelector(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final views = workspaces.workspace.views;
|
||||
return BlocProvider<SettingsFileExporterCubit>(
|
||||
create: (_) => SettingsFileExporterCubit(views: views),
|
||||
child: const _ExpandedList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
void _showToast(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText(
|
||||
message,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
final apps = state.views;
|
||||
final expanded = state.expanded;
|
||||
final selectedItems = state.selectedItems;
|
||||
final isExpaned = expanded[index] == true;
|
||||
final isExpanded = expanded[index] == true;
|
||||
List<Widget> expandedChildren = [];
|
||||
if (isExpaned) {
|
||||
if (isExpanded) {
|
||||
for (var i = 0; i < selectedItems[index].length; i++) {
|
||||
final name = apps[index].childViews[i].name;
|
||||
final checkbox = CheckboxListTile(
|
||||
@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
child: ListTile(
|
||||
title: FlowyText.medium(apps[index].name),
|
||||
trailing: Icon(
|
||||
isExpaned
|
||||
isExpanded
|
||||
? Icons.arrow_drop_down_rounded
|
||||
: Icons.arrow_drop_up_rounded,
|
||||
),
|
||||
@ -182,3 +219,45 @@ extension AppFlowy on dartz.Either {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyFileExporter {
|
||||
static Future<(bool result, List<String> failedNames)> exportToPath(
|
||||
String path,
|
||||
List<ViewPB> views,
|
||||
) async {
|
||||
final failedFileNames = <String>[];
|
||||
final Map<String, int> names = {};
|
||||
final documentService = DocumentService();
|
||||
for (final view in views) {
|
||||
String? content;
|
||||
String? fileExtension;
|
||||
switch (view.layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
final document = await documentService.openDocument(view: view);
|
||||
document.fold((l) => Log.error(l), (r) {
|
||||
final json = r.toDocument()?.toJson();
|
||||
if (json != null) {
|
||||
content = jsonEncode(json);
|
||||
}
|
||||
});
|
||||
fileExtension = 'afdocument';
|
||||
break;
|
||||
default:
|
||||
// TODO(nathan): export the new databse data to json
|
||||
content = null;
|
||||
break;
|
||||
}
|
||||
if (content != null) {
|
||||
final count = names.putIfAbsent(view.name, () => 0);
|
||||
final name = count == 0 ? view.name : '${view.name}($count)';
|
||||
final file = File(p.join(path, '$name.$fileExtension'));
|
||||
await file.writeAsString(content!);
|
||||
names[view.name] = count + 1;
|
||||
} else {
|
||||
failedFileNames.add(view.name);
|
||||
}
|
||||
}
|
||||
|
||||
return (failedFileNames.isEmpty, failedFileNames);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -11,22 +12,18 @@ class SettingsFileSystemView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
|
||||
late final _items = [
|
||||
const SettingsFileLocationCustomizer(),
|
||||
const SettingsExportFileWidget()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// return Column(
|
||||
// children: [],
|
||||
// );
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return const SettingsFileLocationCustomizer();
|
||||
} else if (index == 1) {
|
||||
// return _buildExportDatabaseButton();
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => _items[index],
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemCount: 2, // make the divider taking effect.
|
||||
itemCount: _items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -52,12 +52,11 @@ packages:
|
||||
appflowy_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "9732d30"
|
||||
resolved-ref: "9732d30e852ccb832785d6fff3923966452ffcf4"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "0.1.12"
|
||||
name: appflowy_editor
|
||||
sha256: "3561bd7bd99541508353034130a98ab2d9be54f690bb982f85c2b3eedb8fe63e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0-dev.1"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.1.5
|
||||
version: 0.2.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@ -42,10 +42,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: a183c57
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: 9732d30
|
||||
appflowy_editor: 1.0.0-dev.1
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
|
@ -1,9 +1,20 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('editor migration, from v0.1.x to 0.2', () {
|
||||
test('migrate readme', () {
|
||||
// final
|
||||
test('migrate readme', () async {
|
||||
final readme = await rootBundle.loadString('assets/template/readme.json');
|
||||
final oldDocument = DocumentV0.fromJson(json.decode(readme));
|
||||
final document = EditorMigration.migrateDocument(readme);
|
||||
expect(document.root.type, 'page');
|
||||
expect(oldDocument.root.children.length, document.root.children.length);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -12,11 +12,11 @@ void main() {
|
||||
const text = '''
|
||||
{
|
||||
"document":{
|
||||
"type":"document",
|
||||
"type":"page",
|
||||
"children":[
|
||||
{
|
||||
"type":"math_equation",
|
||||
"attributes":{
|
||||
"data":{
|
||||
"math_equation":"E = MC^2"
|
||||
}
|
||||
}
|
||||
@ -40,11 +40,11 @@ void main() {
|
||||
const text = '''
|
||||
{
|
||||
"document":{
|
||||
"type":"editor",
|
||||
"type":"page",
|
||||
"children":[
|
||||
{
|
||||
"type":"code_block",
|
||||
"attributes":{
|
||||
"data":{
|
||||
"code_block":"Some Code"
|
||||
}
|
||||
}
|
||||
@ -67,7 +67,7 @@ void main() {
|
||||
const text = '''
|
||||
{
|
||||
"document":{
|
||||
"type":"editor",
|
||||
"type":"page",
|
||||
"children":[
|
||||
{
|
||||
"type":"divider"
|
||||
|
1
frontend/rust-lib/Cargo.lock
generated
@ -1692,6 +1692,7 @@ dependencies = [
|
||||
"flowy-derive",
|
||||
"flowy-error",
|
||||
"flowy-notification",
|
||||
"indexmap",
|
||||
"lib-dispatch",
|
||||
"nanoid",
|
||||
"parking_lot 0.12.1",
|
||||
|
219
frontend/rust-lib/flowy-core/assets/read_me.json
Normal file
@ -0,0 +1,219 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 }
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 }
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Click anywhere and just start typing." }],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{
|
||||
"attributes": { "bg_color": "0x4dffeb3b" },
|
||||
"insert": "Highlight "
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "attributes": { "italic": true }, "insert": "style" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "bold": true }, "insert": "your" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "underline": true }, "insert": "writing" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "code": true }, "insert": "however" },
|
||||
{ "insert": " you " },
|
||||
{ "attributes": { "strikethrough": true }, "insert": "like." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "As soon as you type " },
|
||||
{
|
||||
"attributes": { "code": true, "font_color": "0xff00b5ff" },
|
||||
"insert": "/"
|
||||
},
|
||||
{ "insert": " a menu will pop up. Select " },
|
||||
{
|
||||
"attributes": { "bg_color": "0x4d9c27b0" },
|
||||
"insert": "different types"
|
||||
},
|
||||
{ "insert": " of content blocks you can add." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/" },
|
||||
{ "insert": " followed by " },
|
||||
{ "attributes": { "code": true }, "insert": "/bullet" },
|
||||
{ "insert": " or " },
|
||||
{ "attributes": { "code": true }, "insert": "/num" },
|
||||
{ "attributes": { "code": false }, "insert": " to create a list." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+ New Page " },
|
||||
{
|
||||
"insert": "button at the bottom of your sidebar to add a new page."
|
||||
}
|
||||
],
|
||||
"checked": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+" },
|
||||
{ "insert": " next to any page title in the sidebar to " },
|
||||
{
|
||||
"attributes": { "font_color": "0xff8427e0" },
|
||||
"insert": "quickly"
|
||||
},
|
||||
{ "insert": " add a new subpage, " },
|
||||
{ "attributes": { "code": true }, "insert": "Document" },
|
||||
{ "attributes": { "code": false }, "insert": ", " },
|
||||
{ "attributes": { "code": true }, "insert": "Grid" },
|
||||
{ "attributes": { "code": false }, "insert": ", or " },
|
||||
{ "attributes": { "code": true }, "insert": "Kanban Board" },
|
||||
{ "attributes": { "code": false }, "insert": "." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "divider" },
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }],
|
||||
"level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Keyboard shortcuts " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
|
||||
},
|
||||
"insert": "guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Markdown " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
|
||||
},
|
||||
"insert": "reference"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/code" },
|
||||
{
|
||||
"attributes": { "code": false },
|
||||
"insert": " to insert a code block"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"language": "rust",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] }
|
||||
},
|
||||
{
|
||||
"type": "quote",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "?" },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "callout",
|
||||
"data": {
|
||||
"bgColor": "#F0F0F0",
|
||||
"delta": [
|
||||
{ "insert": "\nLike AppFlowy? Follow us:\n" },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
},
|
||||
"insert": "GitHub"
|
||||
},
|
||||
{ "insert": "\n" },
|
||||
{
|
||||
"attributes": { "href": "https://twitter.com/appflowy" },
|
||||
"insert": "Twitter"
|
||||
},
|
||||
{ "insert": ": @appflowy\n" },
|
||||
{
|
||||
"attributes": { "href": "https://blog-appflowy.ghost.io/" },
|
||||
"insert": "Newsletter"
|
||||
},
|
||||
{ "insert": "\n" }
|
||||
],
|
||||
"icon": "🥰"
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "paragraph", "data": { "delta": [] } }
|
||||
]
|
||||
}
|
@ -13,6 +13,7 @@ use flowy_database2::DatabaseManager2;
|
||||
use flowy_document2::document_data::DocumentDataWrapper;
|
||||
use flowy_document2::entities::DocumentDataPB;
|
||||
use flowy_document2::manager::DocumentManager;
|
||||
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_folder2::deps::{FolderCloudService, FolderUser};
|
||||
use flowy_folder2::entities::ViewLayoutPB;
|
||||
@ -113,7 +114,6 @@ impl FolderOperationHandler for DocumentFolderOperation {
|
||||
_ext: HashMap<String, String>,
|
||||
) -> FutureResult<(), FlowyError> {
|
||||
debug_assert_eq!(layout, ViewLayout::Document);
|
||||
// TODO: implement read the document data from custom data.
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
@ -134,11 +134,12 @@ impl FolderOperationHandler for DocumentFolderOperation {
|
||||
) -> FutureResult<(), FlowyError> {
|
||||
debug_assert_eq!(layout, ViewLayout::Document);
|
||||
|
||||
let json_str = include_str!("../../assets/read_me.json");
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
// TODO: implement read the document data from json.
|
||||
FutureResult::new(async move {
|
||||
manager.create_document(view_id, DocumentDataWrapper::default())?;
|
||||
let document_pb = JsonToDocumentParser::json_str_to_document(json_str)?;
|
||||
manager.create_document(view_id, document_pb.into())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
182
frontend/rust-lib/flowy-database/src/services/export.rs
Normal file
@ -0,0 +1,182 @@
|
||||
use crate::entities::FieldType;
|
||||
|
||||
use crate::services::cell::TypeCellData;
|
||||
use crate::services::database::DatabaseEditor;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||
URLCellData,
|
||||
};
|
||||
use database_model::{FieldRevision, TypeOptionDataDeserializer};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ExportField {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub field_type: i64,
|
||||
pub visibility: bool,
|
||||
pub width: i64,
|
||||
pub type_options: HashMap<String, Value>,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ExportCell {
|
||||
data: String,
|
||||
field_type: FieldType,
|
||||
}
|
||||
|
||||
impl From<&Arc<FieldRevision>> for ExportField {
|
||||
fn from(field_rev: &Arc<FieldRevision>) -> Self {
|
||||
let field_type = FieldType::from(field_rev.ty);
|
||||
let mut type_options: HashMap<String, Value> = HashMap::new();
|
||||
|
||||
field_rev
|
||||
.type_options
|
||||
.iter()
|
||||
.filter(|(k, _)| k == &&field_rev.ty.to_string())
|
||||
.for_each(|(k, s)| {
|
||||
let value = match field_type {
|
||||
FieldType::RichText => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Number => {
|
||||
let pb = NumberTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("format".to_string(), json!(pb.format as u8));
|
||||
map.insert("scale".to_string(), json!(pb.scale));
|
||||
map.insert("symbol".to_string(), json!(pb.symbol));
|
||||
map.insert("name".to_string(), json!(pb.name));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::DateTime => {
|
||||
let pb = DateTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("date_format".to_string(), json!(pb.date_format as u8));
|
||||
map.insert("time_format".to_string(), json!(pb.time_format as u8));
|
||||
map.insert("field_type".to_string(), json!(FieldType::DateTime as u8));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::SingleSelect => {
|
||||
let pb = SingleSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::MultiSelect => {
|
||||
let pb = MultiSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::Checkbox => {
|
||||
let pb = CheckboxTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::URL => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Checklist => {
|
||||
let pb = ChecklistTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
};
|
||||
type_options.insert(k.clone(), value);
|
||||
});
|
||||
Self {
|
||||
id: field_rev.id.clone(),
|
||||
name: field_rev.name.clone(),
|
||||
field_type: field_rev.ty as i64,
|
||||
visibility: true,
|
||||
width: 100,
|
||||
type_options,
|
||||
is_primary: field_rev.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CSVExport;
|
||||
impl CSVExport {
|
||||
pub async fn export_database(
|
||||
&self,
|
||||
view_id: &str,
|
||||
database_editor: &Arc<DatabaseEditor>,
|
||||
) -> FlowyResult<String> {
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
let row_revs = database_editor.get_all_row_revs(view_id).await?;
|
||||
let field_revs = database_editor.get_field_revs(None).await?;
|
||||
|
||||
// Write fields
|
||||
let field_records = field_revs
|
||||
.iter()
|
||||
.map(|field| ExportField::from(field))
|
||||
.map(|field| serde_json::to_string(&field).unwrap())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
wtr
|
||||
.write_record(&field_records)
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
|
||||
// Write rows
|
||||
let mut field_by_field_id = IndexMap::new();
|
||||
field_revs.into_iter().for_each(|field| {
|
||||
field_by_field_id.insert(field.id.clone(), field);
|
||||
});
|
||||
for row_rev in row_revs {
|
||||
let cells = field_by_field_id
|
||||
.iter()
|
||||
.map(|(field_id, field)| {
|
||||
let field_type = FieldType::from(field.ty);
|
||||
let data = row_rev
|
||||
.cells
|
||||
.get(field_id)
|
||||
.map(|cell| TypeCellData::try_from(cell))
|
||||
.map(|data| {
|
||||
data
|
||||
.map(|data| match field_type {
|
||||
FieldType::DateTime => {
|
||||
match serde_json::from_str::<DateCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
},
|
||||
FieldType::URL => match serde_json::from_str::<URLCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.content,
|
||||
Err(_) => "".to_string(),
|
||||
},
|
||||
_ => data.cell_str,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let cell = ExportCell { data, field_type };
|
||||
serde_json::to_string(&cell).unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = wtr.write_record(&cells) {
|
||||
tracing::warn!("CSV failed to write record: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let data = wtr
|
||||
.into_inner()
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?;
|
||||
Ok(csv)
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ serde_json = {version = "1.0"}
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
indexmap = {version = "1.9.2", features = ["serde"]}
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.4.0"
|
||||
|
@ -39,7 +39,7 @@ pub struct GetDocumentDataPayloadPB {
|
||||
// Support customize initial data
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
#[derive(Default, Debug, ProtoBuf)]
|
||||
pub struct DocumentDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub page_id: String,
|
||||
@ -69,13 +69,13 @@ pub struct BlockPB {
|
||||
pub children_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct MetaPB {
|
||||
#[pb(index = 1)]
|
||||
pub children_map: HashMap<String, ChildrenPB>,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct ChildrenPB {
|
||||
#[pb(index = 1)]
|
||||
pub children: Vec<String>,
|
||||
|
@ -3,6 +3,7 @@ pub mod document_data;
|
||||
pub mod entities;
|
||||
pub mod event_map;
|
||||
pub mod manager;
|
||||
pub mod parser;
|
||||
pub mod protobuf;
|
||||
|
||||
mod event_handler;
|
||||
|
20
frontend/rust-lib/flowy-document2/src/parser/json/block.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Json format:
|
||||
/// {
|
||||
/// 'type': string,
|
||||
/// 'data': Map<String, Object>
|
||||
/// 'children': [Block],
|
||||
/// }
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Block {
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String,
|
||||
#[serde(default)]
|
||||
pub data: HashMap<String, Value>,
|
||||
#[serde(default)]
|
||||
pub children: Vec<Block>,
|
||||
}
|
2
frontend/rust-lib/flowy-document2/src/parser/json/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod block;
|
||||
pub mod parser;
|
81
frontend/rust-lib/flowy-document2/src/parser/json/parser.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::{collections::HashMap, vec};
|
||||
|
||||
use flowy_error::FlowyResult;
|
||||
use indexmap::IndexMap;
|
||||
use nanoid::nanoid;
|
||||
|
||||
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB};
|
||||
|
||||
use super::block::Block;
|
||||
|
||||
pub struct JsonToDocumentParser;
|
||||
|
||||
impl JsonToDocumentParser {
|
||||
pub fn json_str_to_document(json_str: &str) -> FlowyResult<DocumentDataPB> {
|
||||
let root = serde_json::from_str::<Block>(json_str)?;
|
||||
|
||||
let page_id = nanoid!(10);
|
||||
|
||||
// generate the blocks
|
||||
// the root's parent id is empty
|
||||
let blocks = Self::generate_blocks(&root, Some(page_id.clone()), "".to_string());
|
||||
|
||||
// generate the children map
|
||||
let children_map = Self::generate_children_map(&blocks);
|
||||
|
||||
Ok(DocumentDataPB {
|
||||
page_id,
|
||||
blocks: blocks.into_iter().collect(),
|
||||
meta: MetaPB { children_map },
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_blocks(
|
||||
block: &Block,
|
||||
id: Option<String>,
|
||||
parent_id: String,
|
||||
) -> IndexMap<String, BlockPB> {
|
||||
let block_pb = Self::block_to_block_pb(block, id, parent_id);
|
||||
let mut blocks = IndexMap::new();
|
||||
for child in &block.children {
|
||||
let child_blocks = Self::generate_blocks(child, None, block_pb.id.clone());
|
||||
blocks.extend(child_blocks);
|
||||
}
|
||||
blocks.insert(block_pb.id.clone(), block_pb);
|
||||
blocks
|
||||
}
|
||||
|
||||
fn generate_children_map(blocks: &IndexMap<String, BlockPB>) -> HashMap<String, ChildrenPB> {
|
||||
let mut children_map = HashMap::new();
|
||||
for (id, block) in blocks.iter() {
|
||||
// add itself to it's parent's children
|
||||
if block.parent_id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let parent_block = blocks.get(&block.parent_id);
|
||||
if let Some(parent_block) = parent_block {
|
||||
// insert itself to it's parent's children
|
||||
let children_pb = children_map
|
||||
.entry(parent_block.children_id.clone())
|
||||
.or_insert_with(|| ChildrenPB { children: vec![] });
|
||||
children_pb.children.push(id.clone());
|
||||
// create a children map entry for itself
|
||||
children_map
|
||||
.entry(block.children_id.clone())
|
||||
.or_insert_with(|| ChildrenPB { children: vec![] });
|
||||
}
|
||||
}
|
||||
children_map
|
||||
}
|
||||
|
||||
fn block_to_block_pb(block: &Block, id: Option<String>, parent_id: String) -> BlockPB {
|
||||
let id = id.unwrap_or(nanoid!(10));
|
||||
BlockPB {
|
||||
id: id.clone(),
|
||||
ty: block.ty.clone(),
|
||||
data: serde_json::to_string(&block.data).unwrap(),
|
||||
parent_id: parent_id.clone(),
|
||||
children_id: nanoid!(10),
|
||||
}
|
||||
}
|
||||
}
|
1
frontend/rust-lib/flowy-document2/src/parser/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod json;
|
@ -1 +1,2 @@
|
||||
mod document;
|
||||
mod parser;
|
||||
|
@ -0,0 +1,102 @@
|
||||
use flowy_document2::parser::json::block::Block;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_empty_data_and_children() {
|
||||
let json = json!({
|
||||
"type": "page",
|
||||
});
|
||||
let block = serde_json::from_value::<Block>(json).unwrap();
|
||||
assert_eq!(block.ty, "page");
|
||||
assert!(block.data.is_empty());
|
||||
assert!(block.children.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data() {
|
||||
let json = json!({
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Click anywhere and just start typing." }],
|
||||
"checked": false
|
||||
}
|
||||
});
|
||||
let block = serde_json::from_value::<Block>(json).unwrap();
|
||||
assert_eq!(block.ty, "todo_list");
|
||||
assert_eq!(block.data.len(), 2);
|
||||
assert_eq!(block.data.get("checked").unwrap(), false);
|
||||
assert_eq!(
|
||||
block.data.get("delta").unwrap().to_owned(),
|
||||
json!([{ "insert": "Click anywhere and just start typing." }])
|
||||
);
|
||||
assert!(block.children.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_children() {
|
||||
let json = json!({
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Welcome to AppFlowy!" }],
|
||||
"level": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Welcome to AppFlowy!" }],
|
||||
"checked": false
|
||||
}
|
||||
}
|
||||
]});
|
||||
let block = serde_json::from_value::<Block>(json).unwrap();
|
||||
assert!(block.data.is_empty());
|
||||
assert_eq!(block.ty, "page");
|
||||
assert_eq!(block.children.len(), 2);
|
||||
// heading
|
||||
let heading = &block.children[0];
|
||||
assert_eq!(heading.ty, "heading");
|
||||
assert_eq!(heading.data.len(), 2);
|
||||
|
||||
// todo_list
|
||||
let todo_list = &block.children[1];
|
||||
assert_eq!(todo_list.ty, "todo_list");
|
||||
assert_eq!(todo_list.data.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_children() {
|
||||
let json = json!({
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
let block = serde_json::from_value::<Block>(json).unwrap();
|
||||
assert!(block.data.is_empty());
|
||||
assert_eq!(block.ty, "page");
|
||||
assert_eq!(
|
||||
block.children[0].children[0].children[0].children[0].ty,
|
||||
"paragraph"
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
mod block_test;
|
||||
mod parser_test;
|
@ -0,0 +1,103 @@
|
||||
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_parser_children_in_order() {
|
||||
let json = json!({
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph1",
|
||||
},
|
||||
{
|
||||
"type": "paragraph2",
|
||||
},
|
||||
{
|
||||
"type": "paragraph3",
|
||||
},
|
||||
{
|
||||
"type": "paragraph4",
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let document = JsonToDocumentParser::json_str_to_document(json.to_string().as_str()).unwrap();
|
||||
|
||||
// root + 4 paragraphs
|
||||
assert_eq!(document.blocks.len(), 5);
|
||||
|
||||
// root + 4 paragraphs
|
||||
assert_eq!(document.meta.children_map.len(), 5);
|
||||
|
||||
let (page_id, page_block) = document
|
||||
.blocks
|
||||
.iter()
|
||||
.find(|(_, block)| block.ty == "page")
|
||||
.unwrap();
|
||||
|
||||
// the children should be in order
|
||||
let page_children = document
|
||||
.meta
|
||||
.children_map
|
||||
.get(page_block.children_id.as_str())
|
||||
.unwrap();
|
||||
assert_eq!(page_children.children.len(), 4);
|
||||
for (i, child_id) in page_children.children.iter().enumerate() {
|
||||
let child = document.blocks.get(child_id).unwrap();
|
||||
assert_eq!(child.ty, format!("paragraph{}", i + 1));
|
||||
assert_eq!(child.parent_id, page_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_nested_children() {
|
||||
let json = json!({
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let document = JsonToDocumentParser::json_str_to_document(json.to_string().as_str()).unwrap();
|
||||
|
||||
// root + 4 paragraphs
|
||||
assert_eq!(document.blocks.len(), 5);
|
||||
|
||||
// root + 4 paragraphs
|
||||
assert_eq!(document.meta.children_map.len(), 5);
|
||||
|
||||
let (page_id, page_block) = document
|
||||
.blocks
|
||||
.iter()
|
||||
.find(|(_, block)| block.ty == "page")
|
||||
.unwrap();
|
||||
|
||||
// first child of root is a paragraph
|
||||
let page_children = document
|
||||
.meta
|
||||
.children_map
|
||||
.get(page_block.children_id.as_str())
|
||||
.unwrap();
|
||||
assert_eq!(page_children.children.len(), 1);
|
||||
let page_first_child_id = page_children.children.first().unwrap();
|
||||
let page_first_child = document.blocks.get(page_first_child_id).unwrap();
|
||||
assert_eq!(page_first_child.ty, "paragraph");
|
||||
assert_eq!(page_first_child.parent_id, page_id.to_owned());
|
||||
}
|
1
frontend/rust-lib/flowy-document2/tests/parser/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod json;
|
@ -1,181 +0,0 @@
|
||||
{
|
||||
"document": {
|
||||
"type": "document",
|
||||
"children": [{
|
||||
"type": "heading",
|
||||
"attributes": {
|
||||
"level": 2,
|
||||
"delta": [{
|
||||
"insert": "👋 "
|
||||
},
|
||||
{
|
||||
"insert": "Welcome to",
|
||||
"attributes": {
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "AppFlowy Editor",
|
||||
"attributes": {
|
||||
"href": "appflowy.io",
|
||||
"italic": true,
|
||||
"bold": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "AppFlowy Editor is a"
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "highly customizable",
|
||||
"attributes": {
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "rich-text editor",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " for "
|
||||
},
|
||||
{
|
||||
"insert": "Flutter",
|
||||
"attributes": {
|
||||
"underline": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"attributes": {
|
||||
"checked": true,
|
||||
"delta": [{
|
||||
"insert": "Customizable"
|
||||
}]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"attributes": {
|
||||
"checked": true,
|
||||
"delta": [{
|
||||
"insert": "Test-covered"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"attributes": {
|
||||
"checked": false,
|
||||
"delta": [{
|
||||
"insert": "more to come!"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "quote",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "Here is an example you can give a try"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "You can also use "
|
||||
},
|
||||
{
|
||||
"insert": "AppFlowy Editor",
|
||||
"attributes": {
|
||||
"italic": true,
|
||||
"bold": true,
|
||||
"backgroundColor": "0x6000BCF0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " as a component to build your own app."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bulleted_list",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "Use / to insert blocks"
|
||||
}]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"type": "bulleted_list",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "Select text to trigger to the toolbar to format your notes."
|
||||
}]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attributes": {
|
||||
"delta": [{
|
||||
"insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|