feat: support align and upgrade appflowy_editor (#2712)

* feat: support align and upgrade appflowy_editor

* chore: try to fix linux analyze error

* fix: error after inserting callout block

* feat: add inline board / grid plugin

* feat: refactor insert_page

* fix: ref view in document

* chore: add asset name and description to option align type

* fix: linux flutter analyze

* chore: disable file export and log

* fix: the window always back to center after relaunching

* fix: potential data lost in nested list

* feat: re-design login page

* fix: can't remove background color

* chore: rename bundle id and change the macos app to non sandbox app

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Lucas.Xu
2023-06-06 17:19:53 +08:00
committed by GitHub
parent e96bea0de5
commit e3eee76609
35 changed files with 582 additions and 401 deletions

View File

@ -1,6 +1,5 @@
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show
@ -31,11 +30,12 @@ class TransactionAdapter {
final String documentId;
Future<void> apply(Transaction transaction, EditorState editorState) async {
// Log.debug('transaction => ${transaction.toJson()}');
final actions = transaction.operations
.map((op) => op.toBlockAction(editorState))
.whereNotNull()
.expand((element) => element);
Log.debug('actions => $actions');
// Log.debug('actions => $actions');
await documentService.applyAction(
documentId: documentId,
actions: actions,
@ -85,6 +85,12 @@ extension on InsertOperation {
..action = BlockActionTypePB.Insert
..payload = payload,
);
if (node.children.isNotEmpty) {
final childrenActions = node.children
.map((e) => InsertOperation(e.path, [e]).toBlockAction(editorState))
.expand((element) => element);
actions.addAll(childrenActions);
}
previousNode = node;
}
return actions;

View File

@ -24,16 +24,6 @@ class AppFlowyEditorPage extends StatefulWidget {
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final scrollController = ScrollController();
final slashMenuItems = [
boardMenuItem,
gridMenuItem,
calloutItem,
dividerMenuItem,
mathEquationItem,
codeBlockItem,
emojiMenuItem,
autoGeneratorMenuItem,
];
final List<CommandShortcutEvent> commandShortcutEvents = [
...codeBlockCommands,
@ -53,6 +43,19 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
highlightColorItem,
];
late final slashMenuItems = [
dividerMenuItem,
inlineGridMenuItem(documentBloc),
referenceGridMenuItem,
inlineBoardMenuItem(documentBloc),
boardMenuItem,
calloutItem,
mathEquationItem,
codeBlockItem,
emojiMenuItem,
autoGeneratorMenuItem,
];
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
_customAppFlowyBlockComponentBuilders();
late final List<CharacterShortcutEvent> characterShortcutEvents = [
@ -170,7 +173,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
),
ImageBlockKeys.type: ImageBlockComponentBuilder(),
ImageBlockKeys.type: ImageBlockComponentBuilder(
configuration: configuration,
),
BoardBlockKeys.type: BoardBlockComponentBuilder(
configuration: configuration,
),
@ -225,14 +230,24 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
CalloutBlockKeys.type
];
final supportAlignBuilderType = [
ImageBlockKeys.type,
];
final colorAction = [
OptionAction.divider,
OptionAction.color,
];
final alignAction = [
OptionAction.divider,
OptionAction.align,
];
final List<OptionAction> actions = [
...standardActions,
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
];
builder.showActions = (_) => true;

View File

@ -24,14 +24,15 @@ class BlockOptionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final popoverActions = actions.map((e) {
if (e == OptionAction.divider) {
return DividerOptionAction();
} else if (e == OptionAction.color) {
return ColorOptionAction(
editorState: editorState,
);
} else {
return OptionActionWrapper(e);
switch (e) {
case OptionAction.divider:
return DividerOptionAction();
case OptionAction.color:
return ColorOptionAction(editorState: editorState);
case OptionAction.align:
return AlignOptionAction(editorState: editorState);
default:
return OptionActionWrapper(e);
}
}).toList();
@ -119,6 +120,7 @@ class BlockOptionButton extends StatelessWidget {
case OptionAction.moveDown:
transaction.moveNode(node.path.next.next, node);
break;
case OptionAction.align:
case OptionAction.color:
case OptionAction.divider:
throw UnimplementedError();

View File

@ -1,12 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart';
@ -18,6 +18,90 @@ enum OptionAction {
moveDown,
color,
divider,
align;
String get assetName {
switch (this) {
case OptionAction.delete:
return 'editor/delete';
case OptionAction.duplicate:
return 'editor/duplicate';
case OptionAction.turnInto:
return 'editor/turn_into';
case OptionAction.moveUp:
return 'editor/move_up';
case OptionAction.moveDown:
return 'editor/move_down';
case OptionAction.color:
return 'editor/color';
case OptionAction.divider:
return 'editor/divider';
case OptionAction.align:
return 'editor/align/center';
}
}
String get description {
switch (this) {
case OptionAction.delete:
return LocaleKeys.document_plugins_optionAction_delete.tr();
case OptionAction.duplicate:
return LocaleKeys.document_plugins_optionAction_duplicate.tr();
case OptionAction.turnInto:
return LocaleKeys.document_plugins_optionAction_turnInto.tr();
case OptionAction.moveUp:
return LocaleKeys.document_plugins_optionAction_moveUp.tr();
case OptionAction.moveDown:
return LocaleKeys.document_plugins_optionAction_moveDown.tr();
case OptionAction.color:
return LocaleKeys.document_plugins_optionAction_color.tr();
case OptionAction.align:
return LocaleKeys.document_plugins_optionAction_align.tr();
case OptionAction.divider:
throw UnsupportedError('Divider does not have description');
}
}
}
enum OptionAlignType {
left,
center,
right;
static OptionAlignType fromString(String? value) {
switch (value) {
case 'left':
return OptionAlignType.left;
case 'center':
return OptionAlignType.center;
case 'right':
return OptionAlignType.right;
default:
return OptionAlignType.center;
}
}
String get assetName {
switch (this) {
case OptionAlignType.left:
return 'editor/align/left';
case OptionAlignType.center:
return 'editor/align/center';
case OptionAlignType.right:
return 'editor/align/right';
}
}
String get description {
switch (this) {
case OptionAlignType.left:
return LocaleKeys.document_plugins_optionAction_left.tr();
case OptionAlignType.center:
return LocaleKeys.document_plugins_optionAction_center.tr();
case OptionAlignType.right:
return LocaleKeys.document_plugins_optionAction_right.tr();
}
}
}
class DividerOptionAction extends CustomActionCell {
@ -30,6 +114,98 @@ class DividerOptionAction extends CustomActionCell {
}
}
class AlignOptionAction extends PopoverActionCell {
AlignOptionAction({
required this.editorState,
});
final EditorState editorState;
@override
Widget? leftIcon(Color iconColor) {
return FlowySvg(
name: align.assetName,
size: const Size.square(12),
).padding(all: 2.0);
}
@override
String get name {
return LocaleKeys.document_plugins_optionAction_align.tr();
}
@override
PopoverActionCellBuilder get builder =>
(context, parentController, controller) {
final selection = editorState.selection?.normalized;
if (selection == null) {
return const SizedBox.shrink();
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return const SizedBox.shrink();
}
final children = buildAlignOptions(context, (align) async {
await onAlignChanged(align);
controller.close();
parentController.close();
});
return IntrinsicHeight(
child: IntrinsicWidth(
child: Column(
children: children,
),
),
);
};
List<Widget> buildAlignOptions(
BuildContext context,
void Function(OptionAlignType) onTap,
) {
return OptionAlignType.values.map((e) => OptionAlignWrapper(e)).map((e) {
final leftIcon = e.leftIcon(Theme.of(context).colorScheme.onSurface);
final rightIcon = e.rightIcon(Theme.of(context).colorScheme.onSurface);
return HoverButton(
onTap: () => onTap(e.inner),
itemHeight: ActionListSizes.itemHeight,
leftIcon: leftIcon,
name: e.name,
rightIcon: rightIcon,
);
}).toList();
}
OptionAlignType get align {
final selection = editorState.selection;
if (selection == null) {
return OptionAlignType.center;
}
final node = editorState.getNodeAtPath(selection.start.path);
final align = node?.attributes['align'];
return OptionAlignType.fromString(align);
}
Future<void> onAlignChanged(OptionAlignType align) async {
if (align == this.align) {
return;
}
final selection = editorState.selection;
if (selection == null) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return;
}
final transaction = editorState.transaction;
transaction.updateNode(node, {
'align': align.name,
});
await editorState.apply(transaction);
}
}
class ColorOptionAction extends PopoverActionCell {
ColorOptionAction({
required this.editorState,
@ -46,60 +222,61 @@ class ColorOptionAction extends PopoverActionCell {
}
@override
String get name {
return LocaleKeys.toolbar_color.tr();
}
String get name => LocaleKeys.document_plugins_optionAction_color.tr();
@override
Widget Function(BuildContext context, PopoverController controller)
get builder => (context, controller) {
final selection = editorState.selection?.normalized;
if (selection == null) {
return const SizedBox.shrink();
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return const SizedBox.shrink();
}
final bgColor =
node.attributes[blockComponentBackgroundColor] as String?;
final selectedColor = convertHexToSelectOptionColorPB(
bgColor,
context,
);
return SelectOptionColorList(
selectedColor: selectedColor,
onSelectedColor: (color) {
controller.close();
final nodes = editorState.getNodesInSelection(selection);
final transaction = editorState.transaction;
for (final node in nodes) {
transaction.updateNode(node, {
blockComponentBackgroundColor:
color.toColor(context).toHex(),
});
}
editorState.apply(transaction);
},
);
};
SelectOptionColorPB? convertHexToSelectOptionColorPB(
String? hexColor,
Widget Function(
BuildContext context,
) {
if (hexColor == null) {
return null;
}
for (final value in SelectOptionColorPB.values) {
if (value.toColor(context).toHex() == hexColor) {
return value;
}
}
return null;
}
PopoverController parentController,
PopoverController controller,
) get builder => (context, parentController, controller) {
final selection = editorState.selection?.normalized;
if (selection == null) {
return const SizedBox.shrink();
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return const SizedBox.shrink();
}
final bgColor =
node.attributes[blockComponentBackgroundColor] as String?;
final selectedColor = bgColor?.toColor();
final colors = [
// clear background color.
FlowyColorOption(
color: Colors.transparent,
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
),
...FlowyTint.values.map(
(e) => FlowyColorOption(
color: e.color(context),
name: e.tintName(AppFlowyEditorLocalizations.current),
),
),
];
return FlowyColorPicker(
colors: colors,
selected: selectedColor,
border: Border.all(
color: Theme.of(context).colorScheme.onBackground,
width: 1,
),
onTap: (color, index) async {
final transaction = editorState.transaction;
final backgroundColor =
color == Colors.transparent ? null : color.toHex();
transaction.updateNode(node, {
blockComponentBackgroundColor: backgroundColor,
});
await editorState.apply(transaction);
controller.close();
parentController.close();
},
);
};
}
class OptionActionWrapper extends ActionCell {
@ -108,60 +285,20 @@ class OptionActionWrapper extends ActionCell {
final OptionAction inner;
@override
Widget? leftIcon(Color iconColor) {
var name = '';
// TODO: add icons.
switch (inner) {
case OptionAction.delete:
name = 'editor/delete';
break;
case OptionAction.duplicate:
name = 'editor/duplicate';
break;
case OptionAction.turnInto:
name = 'editor/turn_into';
break;
case OptionAction.moveUp:
name = 'editor/move_up';
break;
case OptionAction.moveDown:
name = 'editor/move_down';
break;
default:
throw UnimplementedError();
}
if (name.isEmpty) {
return null;
}
return FlowySvg(name: name);
}
Widget? leftIcon(Color iconColor) => FlowySvg(name: inner.assetName);
@override
String get name {
var description = '';
switch (inner) {
// TODO: l10n
case OptionAction.delete:
description = LocaleKeys.document_plugins_optionAction_delete.tr();
break;
case OptionAction.duplicate:
description = LocaleKeys.document_plugins_optionAction_duplicate.tr();
break;
case OptionAction.turnInto:
description = LocaleKeys.document_plugins_optionAction_turnInto.tr();
break;
case OptionAction.moveUp:
description = LocaleKeys.document_plugins_optionAction_moveUp.tr();
break;
case OptionAction.moveDown:
description = LocaleKeys.document_plugins_optionAction_moveDown.tr();
break;
case OptionAction.color:
description = LocaleKeys.document_plugins_optionAction_color.tr();
break;
case OptionAction.divider:
throw UnimplementedError();
}
return description;
}
String get name => inner.description;
}
class OptionAlignWrapper extends ActionCell {
OptionAlignWrapper(this.inner);
final OptionAlignType inner;
@override
Widget? leftIcon(Color iconColor) => FlowySvg(name: inner.assetName);
@override
String get name => inner.description;
}

View File

@ -102,6 +102,7 @@ class OptionActionList extends StatelessWidget {
case OptionAction.moveDown:
transaction.moveNode(node.path.next.next, node);
break;
case OptionAction.align:
case OptionAction.color:
case OptionAction.divider:
throw UnimplementedError();

View File

@ -15,7 +15,31 @@ class DatabaseBlockKeys {
}
extension InsertDatabase on EditorState {
Future<void> insertPage(ViewPB parentView, ViewPB childView) async {
Future<void> insertInlinePage(String parentViewId, ViewPB childView) async {
final selection = this.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final transaction = this.transaction;
transaction.insertNode(
selection.end.path,
Node(
type: _convertPageType(childView),
attributes: {
DatabaseBlockKeys.parentID: parentViewId,
DatabaseBlockKeys.viewID: childView.id,
},
),
);
await apply(transaction);
}
Future<void> insertReferencePage(ViewPB childView) async {
final selection = this.selection;
if (selection == null || !selection.isCollapsed) {
return;

View File

@ -35,7 +35,7 @@ void showLinkToPageMenu(
layoutType: pageType,
hintText: pageType.toHintText(),
onSelected: (appPB, viewPB) {
editorState.insertPage(appPB, viewPB);
editorState.insertReferencePage(viewPB);
linkToPageMenuEntry.remove();
},
),

View File

@ -7,7 +7,7 @@ import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
import 'package:easy_localization/easy_localization.dart';
SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem inlineBoardMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem(
name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
icon: (editorState, onSelected, style) => SelectableSvgWidget(
@ -15,43 +15,21 @@ SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
isSelected: onSelected,
style: style,
),
keywords: ['board', 'kanban'],
keywords: ['board', 'kanban', 'database'],
handler: (editorState, menuService, context) async {
if (!documentBloc.view.hasParentViewId()) {
return;
}
final appId = documentBloc.view.parentViewId;
final service = ViewBackendService();
final result = (await ViewBackendService.createView(
parentViewId: appId,
final parentViewId = documentBloc.view.parentViewId;
ViewBackendService.createView(
parentViewId: parentViewId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutPB.Board,
))
.getLeftOrNull();
// If the result is null, then something went wrong here.
if (result == null) {
return;
}
final app = (await service.getView(result.viewId)).getLeftOrNull();
// We should show an error dialog.
if (app == null) {
return;
}
final view = (await service.getChildView(
parentViewId: result.viewId,
childViewId: result.id,
))
.getLeftOrNull();
// As this.
if (view == null) {
return;
}
editorState.insertPage(app, view);
).then(
(value) => value
.swap()
.map((r) => editorState.insertInlinePage(parentViewId, r)),
);
},
);

View File

@ -51,7 +51,7 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node(
nodeBuilder: (editorState) => calloutNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
updateSelection: (_, path, __, ___) {
return Selection.single(path: [...path, 0], startOffset: 0);
return Selection.single(path: path, startOffset: 0);
},
);

View File

@ -6,14 +6,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
SelectionMenuItem gridMenuItem = SelectionMenuItem(
SelectionMenuItem referenceGridMenuItem = SelectionMenuItem(
name: LocaleKeys.document_plugins_referencedGrid.tr(),
icon: (editorState, onSelected, style) => SelectableSvgWidget(
name: 'editor/board',
name: 'editor/grid',
isSelected: onSelected,
style: style,
),
keywords: ['referenced', 'grid'],
keywords: ['referenced', 'grid', 'database'],
handler: (editorState, menuService, context) {
final container = Overlay.of(context);
showLinkToPageMenu(

View File

@ -7,7 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem inlineGridMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem(
name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
icon: (editorState, onSelected, style) => SelectableSvgWidget(
@ -15,43 +15,22 @@ SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
isSelected: onSelected,
style: style,
),
keywords: ['grid'],
keywords: ['grid', 'database'],
handler: (editorState, menuService, context) async {
if (!documentBloc.view.hasParentViewId()) {
return;
}
final appId = documentBloc.view.parentViewId;
final service = ViewBackendService();
final result = (await ViewBackendService.createView(
parentViewId: appId,
final parentViewId = documentBloc.view.parentViewId;
ViewBackendService.createView(
parentViewId: parentViewId,
openAfterCreate: false,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutPB.Grid,
))
.getLeftOrNull();
// If the result is null, then something went wrong here.
if (result == null) {
return;
}
final app = (await service.getView(result.viewId)).getLeftOrNull();
// We should show an error dialog.
if (app == null) {
return;
}
final view = (await service.getChildView(
parentViewId: result.viewId,
childViewId: result.id,
))
.getLeftOrNull();
// As this.
if (view == null) {
return;
}
editorState.insertPage(app, view);
).then(
(value) => value
.swap()
.map((r) => editorState.insertInlinePage(parentViewId, r)),
);
},
);