mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Integrate Grid into Document (#1759)
* fix: cursor doesn't blink when opening selection menu * feat: add board plugin * feat: integrate board plugin into document * feat: add i10n and fix known bugs * feat: support jump to board page on document * feat: disable editor scroll only when the board plugin is selected * chore: dart fix * chore: remove unused files * fix: dart lint * feat: integrate grid plugin into document * feat: add more menu to grid plugins * feat: refactor built-in page plugins, including board and grid * feat: remove padding set up when plugin type equals to editor
This commit is contained in:
parent
71022ed934
commit
2e91dfb4be
@ -317,7 +317,10 @@
|
|||||||
},
|
},
|
||||||
"slashMenu": {
|
"slashMenu": {
|
||||||
"board": {
|
"board": {
|
||||||
"selectABoardToLinkTo": "Select a board to link to"
|
"selectABoardToLinkTo": "Select a Board to link to"
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"selectAGridToLinkTo": "Select a Grid to link to"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
|
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
|
||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
@ -111,6 +113,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
||||||
// Board
|
// Board
|
||||||
kBoardType: BoardNodeWidgetBuilder(),
|
kBoardType: BoardNodeWidgetBuilder(),
|
||||||
|
// Grid
|
||||||
|
kGridType: GridNodeWidgetBuilder(),
|
||||||
// Card
|
// Card
|
||||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
kCalloutType: CalloutNodeWidgetBuilder(),
|
||||||
},
|
},
|
||||||
@ -133,6 +137,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
emojiMenuItem,
|
emojiMenuItem,
|
||||||
// Board
|
// Board
|
||||||
boardMenuItem,
|
boardMenuItem,
|
||||||
|
// Grid
|
||||||
|
gridMenuItem,
|
||||||
],
|
],
|
||||||
themeData: theme.copyWith(extensions: [
|
themeData: theme.copyWith(extensions: [
|
||||||
...theme.extensions.values,
|
...theme.extensions.values,
|
||||||
|
@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
|
|||||||
? EditorStyle.dark
|
? EditorStyle.dark
|
||||||
: EditorStyle.light;
|
: EditorStyle.light;
|
||||||
editorStyle = editorStyle.copyWith(
|
editorStyle = editorStyle.copyWith(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
padding: const EdgeInsets.symmetric(horizontal: 100),
|
||||||
textStyle: editorStyle.textStyle?.copyWith(
|
textStyle: editorStyle.textStyle?.copyWith(
|
||||||
fontFamily: 'poppins',
|
fontFamily: 'poppins',
|
||||||
fontSize: documentStyle.fontSize,
|
fontSize: documentStyle.fontSize,
|
||||||
|
@ -0,0 +1,160 @@
|
|||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||||
|
import 'package:app_flowy/startup/startup.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||||
|
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
||||||
|
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:dartz/dartz.dart' as dartz;
|
||||||
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
|
||||||
|
class BuiltInPageWidget extends StatefulWidget {
|
||||||
|
const BuiltInPageWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.node,
|
||||||
|
required this.editorState,
|
||||||
|
required this.builder,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final EditorState editorState;
|
||||||
|
final Widget Function(ViewPB viewPB) builder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BuiltInPageWidget> createState() => _BuiltInPageWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
String get gridID {
|
||||||
|
return widget.node.attributes[kViewID];
|
||||||
|
}
|
||||||
|
|
||||||
|
String get appID {
|
||||||
|
return widget.node.attributes[kAppID];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final board = snapshot.data?.getLeftOrNull<ViewPB>();
|
||||||
|
if (board != null) {
|
||||||
|
return _build(context, board);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
future: AppService().getView(appID, gridID),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _build(BuildContext context, ViewPB viewPB) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (event) {
|
||||||
|
widget.editorState.service.scrollService?.disable();
|
||||||
|
},
|
||||||
|
onExit: (event) {
|
||||||
|
widget.editorState.service.scrollService?.enable();
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
height: 400,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildMenu(context, viewPB),
|
||||||
|
_buildGrid(context, viewPB),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGrid(BuildContext context, ViewPB viewPB) {
|
||||||
|
return Focus(
|
||||||
|
focusNode: focusNode,
|
||||||
|
onFocusChange: (value) {
|
||||||
|
if (value) {
|
||||||
|
widget.editorState.service.selectionService.clearSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: widget.builder(viewPB),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenu(BuildContext context, ViewPB viewPB) {
|
||||||
|
return Positioned(
|
||||||
|
top: 5,
|
||||||
|
left: 5,
|
||||||
|
child: PopoverActionList<_ActionWrapper>(
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
actions:
|
||||||
|
_ActionType.values.map((action) => _ActionWrapper(action)).toList(),
|
||||||
|
buildChild: (controller) {
|
||||||
|
return FlowyIconButton(
|
||||||
|
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
|
||||||
|
width: 25,
|
||||||
|
height: 30,
|
||||||
|
iconPadding: const EdgeInsets.all(3),
|
||||||
|
icon: svgWidget('editor/details'),
|
||||||
|
onPressed: () => controller.show(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (action, controller) async {
|
||||||
|
switch (action.inner) {
|
||||||
|
case _ActionType.openAsPage:
|
||||||
|
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||||
|
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
||||||
|
break;
|
||||||
|
case _ActionType.delete:
|
||||||
|
final transaction = widget.editorState.transaction;
|
||||||
|
transaction.deleteNode(widget.node);
|
||||||
|
widget.editorState.apply(transaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ActionType {
|
||||||
|
openAsPage,
|
||||||
|
delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionWrapper extends ActionCell {
|
||||||
|
final _ActionType inner;
|
||||||
|
|
||||||
|
_ActionWrapper(this.inner);
|
||||||
|
|
||||||
|
Widget? icon(Color iconColor) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name {
|
||||||
|
switch (inner) {
|
||||||
|
case _ActionType.openAsPage:
|
||||||
|
return LocaleKeys.tooltip_openAsPage.tr();
|
||||||
|
case _ActionType.delete:
|
||||||
|
return LocaleKeys.disclosureAction_delete.tr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
const String kAppID = 'app_id';
|
||||||
|
const String kViewID = 'view_id';
|
||||||
|
|
||||||
|
extension InsertPage on EditorState {
|
||||||
|
void insertPage(AppPB appPB, ViewPB viewPB) {
|
||||||
|
final selection = service.selectionService.currentSelection.value;
|
||||||
|
final textNodes =
|
||||||
|
service.selectionService.currentSelectedNodes.whereType<TextNode>();
|
||||||
|
if (selection == null || textNodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final transaction = this.transaction;
|
||||||
|
transaction.insertNode(
|
||||||
|
selection.end.path,
|
||||||
|
Node(
|
||||||
|
type: _convertPageType(viewPB),
|
||||||
|
attributes: {
|
||||||
|
kAppID: appPB.id,
|
||||||
|
kViewID: viewPB.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _convertPageType(ViewPB viewPB) {
|
||||||
|
switch (viewPB.layout) {
|
||||||
|
case ViewLayoutTypePB.Grid:
|
||||||
|
return kGridType;
|
||||||
|
case ViewLayoutTypePB.Board:
|
||||||
|
return kBoardType;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown layout type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:dartz/dartz.dart' as dartz;
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'insert_page_command.dart';
|
||||||
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
EditorState? _editorState;
|
||||||
|
OverlayEntry? _linkToPageMenu;
|
||||||
|
|
||||||
|
void showLinkToPageMenu(
|
||||||
|
EditorState editorState,
|
||||||
|
SelectionMenuService menuService,
|
||||||
|
BuildContext context,
|
||||||
|
ViewLayoutTypePB pageType,
|
||||||
|
) {
|
||||||
|
final aligment = menuService.alignment;
|
||||||
|
final offset = menuService.offset;
|
||||||
|
menuService.dismiss();
|
||||||
|
|
||||||
|
_editorState = editorState;
|
||||||
|
|
||||||
|
String hintText = '';
|
||||||
|
switch (pageType) {
|
||||||
|
case ViewLayoutTypePB.Grid:
|
||||||
|
hintText = LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
|
||||||
|
break;
|
||||||
|
case ViewLayoutTypePB.Board:
|
||||||
|
hintText = LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown layout type');
|
||||||
|
}
|
||||||
|
|
||||||
|
_linkToPageMenu?.remove();
|
||||||
|
_linkToPageMenu = OverlayEntry(builder: (context) {
|
||||||
|
return Positioned(
|
||||||
|
top: aligment == Alignment.bottomLeft ? offset.dy : null,
|
||||||
|
bottom: aligment == Alignment.topLeft ? offset.dy : null,
|
||||||
|
left: offset.dx,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: LinkToPageMenu(
|
||||||
|
editorState: editorState,
|
||||||
|
layoutType: pageType,
|
||||||
|
hintText: hintText,
|
||||||
|
onSelected: (appPB, viewPB) {
|
||||||
|
editorState.insertPage(appPB, viewPB);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Overlay.of(context)?.insert(_linkToPageMenu!);
|
||||||
|
|
||||||
|
editorState.service.selectionService.currentSelection
|
||||||
|
.addListener(dismissLinkToPageMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismissLinkToPageMenu() {
|
||||||
|
_linkToPageMenu?.remove();
|
||||||
|
_linkToPageMenu = null;
|
||||||
|
|
||||||
|
_editorState?.service.selectionService.currentSelection
|
||||||
|
.removeListener(dismissLinkToPageMenu);
|
||||||
|
_editorState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkToPageMenu extends StatefulWidget {
|
||||||
|
const LinkToPageMenu({
|
||||||
|
super.key,
|
||||||
|
required this.editorState,
|
||||||
|
required this.layoutType,
|
||||||
|
required this.hintText,
|
||||||
|
required this.onSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EditorState editorState;
|
||||||
|
final ViewLayoutTypePB layoutType;
|
||||||
|
final String hintText;
|
||||||
|
final void Function(AppPB appPB, ViewPB viewPB) onSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||||
|
EditorStyle get style => widget.editorState.editorStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
width: 300,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: style.selectionMenuBackgroundColor,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5,
|
||||||
|
spreadRadius: 1,
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
),
|
||||||
|
child: _buildListWidget(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListWidget(BuildContext context) {
|
||||||
|
return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData &&
|
||||||
|
snapshot.connectionState == ConnectionState.done) {
|
||||||
|
final apps = snapshot.data;
|
||||||
|
final children = <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
widget.hintText,
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (apps != null && apps.isNotEmpty) {
|
||||||
|
for (final app in apps) {
|
||||||
|
if (app.value2.isNotEmpty) {
|
||||||
|
children.add(
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
app.value1.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (final value in app.value2) {
|
||||||
|
children.add(
|
||||||
|
FlowyButton(
|
||||||
|
leftIcon: svgWidget(
|
||||||
|
_iconName(value),
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(value.name),
|
||||||
|
onTap: () => widget.onSelected(app.value1, value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
future: AppService().fetchViews(widget.layoutType),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _iconName(ViewPB viewPB) {
|
||||||
|
switch (viewPB.layout) {
|
||||||
|
case ViewLayoutTypePB.Grid:
|
||||||
|
return 'editor/grid';
|
||||||
|
case ViewLayoutTypePB.Board:
|
||||||
|
return 'editor/board';
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown layout type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,9 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
||||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:dartz/dartz.dart' as dartz;
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
||||||
@ -22,174 +17,13 @@ SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
|||||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
keywords: ['board'],
|
keywords: ['board', 'kanban'],
|
||||||
handler: _showLinkToPageMenu,
|
handler: (editorState, menuService, context) {
|
||||||
|
showLinkToPageMenu(
|
||||||
|
editorState,
|
||||||
|
menuService,
|
||||||
|
context,
|
||||||
|
ViewLayoutTypePB.Board,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
EditorState? _editorState;
|
|
||||||
OverlayEntry? _linkToPageMenu;
|
|
||||||
void _dismissLinkToPageMenu() {
|
|
||||||
_linkToPageMenu?.remove();
|
|
||||||
_linkToPageMenu = null;
|
|
||||||
|
|
||||||
_editorState?.service.selectionService.currentSelection
|
|
||||||
.removeListener(_dismissLinkToPageMenu);
|
|
||||||
_editorState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showLinkToPageMenu(
|
|
||||||
EditorState editorState,
|
|
||||||
SelectionMenuService menuService,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final aligment = menuService.alignment;
|
|
||||||
final offset = menuService.offset;
|
|
||||||
menuService.dismiss();
|
|
||||||
|
|
||||||
_editorState = editorState;
|
|
||||||
|
|
||||||
_linkToPageMenu?.remove();
|
|
||||||
_linkToPageMenu = OverlayEntry(builder: (context) {
|
|
||||||
return Positioned(
|
|
||||||
top: aligment == Alignment.bottomLeft ? offset.dy : null,
|
|
||||||
bottom: aligment == Alignment.topLeft ? offset.dy : null,
|
|
||||||
left: offset.dx,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: LinkToPageMenu(
|
|
||||||
editorState: editorState,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Overlay.of(context)?.insert(_linkToPageMenu!);
|
|
||||||
|
|
||||||
editorState.service.selectionService.currentSelection
|
|
||||||
.addListener(_dismissLinkToPageMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LinkToPageMenu extends StatefulWidget {
|
|
||||||
final EditorState editorState;
|
|
||||||
|
|
||||||
const LinkToPageMenu({
|
|
||||||
super.key,
|
|
||||||
required this.editorState,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|
||||||
EditorStyle get style => widget.editorState.editorStyle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.transparent,
|
|
||||||
width: 300,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: style.selectionMenuBackgroundColor,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
blurRadius: 5,
|
|
||||||
spreadRadius: 1,
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
|
||||||
),
|
|
||||||
child: _buildBoardListWidget(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<dartz.Tuple2<AppPB, List<ViewPB>>>> fetchBoards() async {
|
|
||||||
return AppService().fetchViews(ViewLayoutTypePB.Board);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBoardListWidget(BuildContext context) {
|
|
||||||
return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData &&
|
|
||||||
snapshot.connectionState == ConnectionState.done) {
|
|
||||||
final apps = snapshot.data;
|
|
||||||
final children = <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(),
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
if (apps != null && apps.isNotEmpty) {
|
|
||||||
for (final app in apps) {
|
|
||||||
if (app.value2.isNotEmpty) {
|
|
||||||
children.add(
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
app.value1.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for (final board in app.value2) {
|
|
||||||
children.add(
|
|
||||||
FlowyButton(
|
|
||||||
leftIcon: svgWidget(
|
|
||||||
'editor/board',
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
text: FlowyText.regular(board.name),
|
|
||||||
onTap: () => widget.editorState.insertBoard(
|
|
||||||
app.value1,
|
|
||||||
board,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
future: fetchBoards(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on EditorState {
|
|
||||||
void insertBoard(AppPB appPB, ViewPB viewPB) {
|
|
||||||
final selection = service.selectionService.currentSelection.value;
|
|
||||||
final textNodes =
|
|
||||||
service.selectionService.currentSelectedNodes.whereType<TextNode>();
|
|
||||||
if (selection == null || textNodes.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final transaction = this.transaction;
|
|
||||||
transaction.insertNode(
|
|
||||||
selection.end.path,
|
|
||||||
Node(
|
|
||||||
type: kBoardType,
|
|
||||||
attributes: {
|
|
||||||
kAppID: appPB.id,
|
|
||||||
kBoardID: viewPB.id,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
apply(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import 'package:app_flowy/plugins/board/presentation/board_page.dart';
|
import 'package:app_flowy/plugins/board/presentation/board_page.dart';
|
||||||
import 'package:app_flowy/startup/startup.dart';
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
||||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||||
import 'package:app_flowy/workspace/application/view/view_ext.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:dartz/dartz.dart' as dartz;
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const String kBoardType = 'board';
|
const String kBoardType = 'board';
|
||||||
const String kAppID = 'app_id';
|
|
||||||
const String kBoardID = 'board_id';
|
|
||||||
|
|
||||||
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
@override
|
@override
|
||||||
@ -27,7 +18,7 @@ class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
NodeValidator<Node> get nodeValidator => (node) {
|
NodeValidator<Node> get nodeValidator => (node) {
|
||||||
return node.attributes[kBoardID] is String &&
|
return node.attributes[kViewID] is String &&
|
||||||
node.attributes[kAppID] is String;
|
node.attributes[kAppID] is String;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -46,130 +37,18 @@ class _BoardWidget extends StatefulWidget {
|
|||||||
State<_BoardWidget> createState() => _BoardWidgetState();
|
State<_BoardWidget> createState() => _BoardWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BoardWidgetState extends State<_BoardWidget> with SelectableMixin {
|
class _BoardWidgetState extends State<_BoardWidget> {
|
||||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
|
||||||
|
|
||||||
String get boardID {
|
|
||||||
return widget.node.attributes[kBoardID];
|
|
||||||
}
|
|
||||||
|
|
||||||
String get appID {
|
|
||||||
return widget.node.attributes[kAppID];
|
|
||||||
}
|
|
||||||
|
|
||||||
late Future<dartz.Either<ViewPB, FlowyError>> board;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
board = _fetchBoard();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
|
return BuiltInPageWidget(
|
||||||
builder: (context, snapshot) {
|
node: widget.node,
|
||||||
if (snapshot.hasData) {
|
editorState: widget.editorState,
|
||||||
final board = snapshot.data?.getLeftOrNull<ViewPB>();
|
builder: (viewPB) {
|
||||||
if (board != null) {
|
return BoardPage(
|
||||||
return _buildBoard(context, board);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
future: board,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<dartz.Either<ViewPB, FlowyError>> _fetchBoard() async {
|
|
||||||
return AppService().getView(appID, boardID);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBoard(BuildContext context, ViewPB viewPB) {
|
|
||||||
return MouseRegion(
|
|
||||||
onHover: (event) {
|
|
||||||
if (widget.node.isSelected(widget.editorState)) {
|
|
||||||
widget.editorState.service.scrollService?.disable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onExit: (event) {
|
|
||||||
widget.editorState.service.scrollService?.enable();
|
|
||||||
},
|
|
||||||
child: SizedBox(
|
|
||||||
height: 400,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
left: 20,
|
|
||||||
child: FlowyTextButton(
|
|
||||||
viewPB.name,
|
|
||||||
onPressed: () {
|
|
||||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
|
||||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BoardPage(
|
|
||||||
key: ValueKey(viewPB.id),
|
key: ValueKey(viewPB.id),
|
||||||
view: viewPB,
|
view: viewPB,
|
||||||
onEditStateChanged: () {
|
);
|
||||||
/// Clear selection when the edit state changes, otherwise the editor will prevent the keyboard event when the board is in edit mode.
|
|
||||||
widget.editorState.service.selectionService.clearSelection();
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool get shouldCursorBlink => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
CursorStyle get cursorStyle => CursorStyle.borderLine;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position start() {
|
|
||||||
return Position(path: widget.node.path, offset: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position end() {
|
|
||||||
return Position(path: widget.node.path, offset: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Position getPositionInOffset(Offset start) {
|
|
||||||
return end();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
|
||||||
return [Offset.zero & _renderBox.size];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
|
||||||
final size = _renderBox.size;
|
|
||||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Selection getSelectionInRange(Offset start, Offset end) {
|
|
||||||
return Selection.single(
|
|
||||||
path: widget.node.path,
|
|
||||||
startOffset: 0,
|
|
||||||
endOffset: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Offset localToGlobal(Offset offset) {
|
|
||||||
return _renderBox.localToGlobal(offset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
SelectionMenuItem gridMenuItem = SelectionMenuItem(
|
||||||
|
name: () => LocaleKeys.grid_menuName.tr(),
|
||||||
|
icon: (editorState, onSelected) {
|
||||||
|
return svgWidget(
|
||||||
|
'editor/grid',
|
||||||
|
size: const Size.square(18.0),
|
||||||
|
color: onSelected
|
||||||
|
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||||
|
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
keywords: ['grid'],
|
||||||
|
handler: (editorState, menuService, context) {
|
||||||
|
showLinkToPageMenu(
|
||||||
|
editorState,
|
||||||
|
menuService,
|
||||||
|
context,
|
||||||
|
ViewLayoutTypePB.Grid,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
||||||
|
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||||
|
import 'package:app_flowy/plugins/grid/presentation/grid_page.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const String kGridType = 'grid';
|
||||||
|
|
||||||
|
class GridNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
|
@override
|
||||||
|
Widget build(NodeWidgetContext<Node> context) {
|
||||||
|
return _GridWidget(
|
||||||
|
key: context.node.key,
|
||||||
|
node: context.node,
|
||||||
|
editorState: context.editorState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeValidator<Node> get nodeValidator => (node) {
|
||||||
|
return node.attributes[kAppID] is String &&
|
||||||
|
node.attributes[kViewID] is String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridWidget extends StatefulWidget {
|
||||||
|
const _GridWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.node,
|
||||||
|
required this.editorState,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_GridWidget> createState() => _GridWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridWidgetState extends State<_GridWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BuiltInPageWidget(
|
||||||
|
node: widget.node,
|
||||||
|
editorState: widget.editorState,
|
||||||
|
builder: (viewPB) {
|
||||||
|
return GridPage(
|
||||||
|
key: ValueKey(viewPB.id),
|
||||||
|
view: viewPB,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -105,8 +105,8 @@ class ShareActionList extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
case ShareAction.copyLink:
|
case ShareAction.copyLink:
|
||||||
NavigatorAlertDialog(
|
NavigatorAlertDialog(
|
||||||
title: LocaleKeys.shareAction_workInProgress.tr())
|
title: LocaleKeys.shareAction_workInProgress.tr(),
|
||||||
.show(context);
|
).show(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
@ -128,5 +128,12 @@ class ShareActionWrapper extends ActionCell {
|
|||||||
Widget? icon(Color iconColor) => null;
|
Widget? icon(Color iconColor) => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => inner.name;
|
String get name {
|
||||||
|
switch (inner) {
|
||||||
|
case ShareAction.markdown:
|
||||||
|
return LocaleKeys.shareAction_markdown.tr();
|
||||||
|
case ShareAction.copyLink:
|
||||||
|
return LocaleKeys.shareAction_copyLink.tr();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,6 @@ import 'widgets/shortcuts.dart';
|
|||||||
import 'widgets/toolbar/grid_toolbar.dart';
|
import 'widgets/toolbar/grid_toolbar.dart';
|
||||||
|
|
||||||
class GridPage extends StatefulWidget {
|
class GridPage extends StatefulWidget {
|
||||||
final ViewPB view;
|
|
||||||
final GridController gridController;
|
|
||||||
final VoidCallback? onDeleted;
|
|
||||||
|
|
||||||
GridPage({
|
GridPage({
|
||||||
required this.view,
|
required this.view,
|
||||||
this.onDeleted,
|
this.onDeleted,
|
||||||
@ -42,6 +38,10 @@ class GridPage extends StatefulWidget {
|
|||||||
}) : gridController = GridController(view: view),
|
}) : gridController = GridController(view: view),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final GridController gridController;
|
||||||
|
final VoidCallback? onDeleted;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GridPage> createState() => _GridPageState();
|
State<GridPage> createState() => _GridPageState();
|
||||||
}
|
}
|
||||||
|
@ -174,9 +174,13 @@ class HomeStackManager {
|
|||||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.ty),
|
index: getIt<PluginSandbox>().indexOf(notifier.plugin.ty),
|
||||||
children: getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
|
children: getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
|
||||||
if (pluginType == notifier.plugin.ty) {
|
if (pluginType == notifier.plugin.ty) {
|
||||||
return notifier.plugin.display
|
final pluginWidget = notifier.plugin.display
|
||||||
.buildWidget(PluginContext(onDeleted: onDeleted))
|
.buildWidget(PluginContext(onDeleted: onDeleted));
|
||||||
.padding(horizontal: 40, vertical: 28);
|
if (pluginType == PluginType.editor) {
|
||||||
|
return pluginWidget;
|
||||||
|
} else {
|
||||||
|
return pluginWidget.padding(horizontal: 40, vertical: 28);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return const BlankPage();
|
return const BlankPage();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user