mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
integrate board plugin into document (#1675)
* 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
This commit is contained in:
parent
0d8adaa921
commit
5de3912fe3
@ -314,12 +314,18 @@
|
|||||||
"date": {
|
"date": {
|
||||||
"timeHintTextInTwelveHour": "01:00 PM",
|
"timeHintTextInTwelveHour": "01:00 PM",
|
||||||
"timeHintTextInTwentyFourHour": "13:00"
|
"timeHintTextInTwentyFourHour": "13:00"
|
||||||
|
},
|
||||||
|
"slashMenu": {
|
||||||
|
"board": {
|
||||||
|
"selectABoardToLinkTo": "Select a board to link to"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"board": {
|
"board": {
|
||||||
"column": {
|
"column": {
|
||||||
"create_new_card": "New"
|
"create_new_card": "New"
|
||||||
}
|
},
|
||||||
|
"menuName": "Board"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"menuName": "Calendar",
|
"menuName": "Calendar",
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
import 'package:app_flowy/plugins/util.dart';
|
import 'package:app_flowy/plugins/util.dart';
|
||||||
|
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'presentation/board_page.dart';
|
import 'presentation/board_page.dart';
|
||||||
@ -18,7 +20,7 @@ class BoardPluginBuilder implements PluginBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get menuName => "Board";
|
String get menuName => LocaleKeys.board_menuName.tr();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get menuIcon => "editor/board";
|
String get menuIcon => "editor/board";
|
||||||
|
@ -4,34 +4,40 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||||
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
|
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||||
|
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-grid/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-grid/row_entities.pb.dart';
|
||||||
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../application/board_bloc.dart';
|
import '../application/board_bloc.dart';
|
||||||
import 'card/card.dart';
|
import 'card/card.dart';
|
||||||
import 'card/card_cell_builder.dart';
|
import 'card/card_cell_builder.dart';
|
||||||
import 'toolbar/board_toolbar.dart';
|
import 'toolbar/board_toolbar.dart';
|
||||||
|
|
||||||
class BoardPage extends StatelessWidget {
|
class BoardPage extends StatelessWidget {
|
||||||
final ViewPB view;
|
|
||||||
BoardPage({
|
BoardPage({
|
||||||
required this.view,
|
required this.view,
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.onEditStateChanged,
|
||||||
}) : super(key: ValueKey(view.id));
|
}) : super(key: ValueKey(view.id));
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
/// Called when edit state changed
|
||||||
|
final VoidCallback? onEditStateChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
@ -45,7 +51,9 @@ class BoardPage extends StatelessWidget {
|
|||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
finish: (result) {
|
finish: (result) {
|
||||||
return result.successOrFail.fold(
|
return result.successOrFail.fold(
|
||||||
(_) => const BoardContent(),
|
(_) => BoardContent(
|
||||||
|
onEditStateChanged: onEditStateChanged,
|
||||||
|
),
|
||||||
(err) => FlowyErrorPage(err.toString()),
|
(err) => FlowyErrorPage(err.toString()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -57,7 +65,12 @@ class BoardPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BoardContent extends StatefulWidget {
|
class BoardContent extends StatefulWidget {
|
||||||
const BoardContent({Key? key}) : super(key: key);
|
const BoardContent({
|
||||||
|
Key? key,
|
||||||
|
this.onEditStateChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final VoidCallback? onEditStateChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BoardContent> createState() => _BoardContentState();
|
State<BoardContent> createState() => _BoardContentState();
|
||||||
@ -79,7 +92,10 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<BoardBloc, BoardState>(
|
return BlocListener<BoardBloc, BoardState>(
|
||||||
listener: (context, state) => _handleEditStateChanged(state, context),
|
listener: (context, state) {
|
||||||
|
_handleEditStateChanged(state, context);
|
||||||
|
widget.onEditStateChanged?.call();
|
||||||
|
},
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
child: BlocBuilder<BoardBloc, BoardState>(
|
||||||
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
|
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
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: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';
|
||||||
@ -97,7 +99,6 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
|
|
||||||
Widget _renderAppFlowyEditor(EditorState editorState) {
|
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final editorMaxWidth = MediaQuery.of(context).size.width * 0.6;
|
|
||||||
final editor = AppFlowyEditor(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
autoFocus: editorState.document.isEmpty,
|
autoFocus: editorState.document.isEmpty,
|
||||||
@ -108,6 +109,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
||||||
// Code Block
|
// Code Block
|
||||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
||||||
|
// Board
|
||||||
|
kBoardType: BoardNodeWidgetBuilder(),
|
||||||
// Card
|
// Card
|
||||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
kCalloutType: CalloutNodeWidgetBuilder(),
|
||||||
},
|
},
|
||||||
@ -128,6 +131,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
codeBlockMenuItem,
|
codeBlockMenuItem,
|
||||||
// Emoji
|
// Emoji
|
||||||
emojiMenuItem,
|
emojiMenuItem,
|
||||||
|
// Board
|
||||||
|
boardMenuItem,
|
||||||
],
|
],
|
||||||
themeData: theme.copyWith(extensions: [
|
themeData: theme.copyWith(extensions: [
|
||||||
...theme.extensions.values,
|
...theme.extensions.values,
|
||||||
@ -138,8 +143,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
return Expanded(
|
return Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: editorMaxWidth,
|
maxWidth: double.infinity,
|
||||||
),
|
),
|
||||||
child: editor,
|
child: editor,
|
||||||
),
|
),
|
||||||
|
@ -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.all(0),
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
textStyle: editorStyle.textStyle?.copyWith(
|
textStyle: editorStyle.textStyle?.copyWith(
|
||||||
fontFamily: 'poppins',
|
fontFamily: 'poppins',
|
||||||
fontSize: documentStyle.fontSize,
|
fontSize: documentStyle.fontSize,
|
||||||
|
@ -0,0 +1,195 @@
|
|||||||
|
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/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:easy_localization/easy_localization.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';
|
||||||
|
|
||||||
|
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
||||||
|
name: () => LocaleKeys.board_menuName.tr(),
|
||||||
|
icon: (editorState, onSelected) {
|
||||||
|
return svgWidget(
|
||||||
|
'editor/board',
|
||||||
|
size: const Size.square(18.0),
|
||||||
|
color: onSelected
|
||||||
|
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||||
|
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
keywords: ['board'],
|
||||||
|
handler: _showLinkToPageMenu,
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
import 'package:app_flowy/plugins/board/presentation/board_page.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: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:dartz/dartz.dart' as dartz;
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const String kBoardType = 'board';
|
||||||
|
const String kAppID = 'app_id';
|
||||||
|
const String kBoardID = 'board_id';
|
||||||
|
|
||||||
|
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
|
@override
|
||||||
|
Widget build(NodeWidgetContext<Node> context) {
|
||||||
|
return _BoardWidget(
|
||||||
|
key: context.node.key,
|
||||||
|
node: context.node,
|
||||||
|
editorState: context.editorState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeValidator<Node> get nodeValidator => (node) {
|
||||||
|
return node.attributes[kBoardID] is String &&
|
||||||
|
node.attributes[kAppID] is String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BoardWidget extends StatefulWidget {
|
||||||
|
const _BoardWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.node,
|
||||||
|
required this.editorState,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BoardWidget> createState() => _BoardWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BoardWidgetState extends State<_BoardWidget> with SelectableMixin {
|
||||||
|
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
|
||||||
|
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 _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),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
@ -77,4 +78,52 @@ class AppService {
|
|||||||
|
|
||||||
return FolderEventMoveFolderItem(payload).send();
|
return FolderEventMoveFolderItem(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Tuple2<AppPB, List<ViewPB>>>> fetchViews(
|
||||||
|
ViewLayoutTypePB layoutType) async {
|
||||||
|
final result = <Tuple2<AppPB, List<ViewPB>>>[];
|
||||||
|
return FolderEventReadCurrentWorkspace().send().then((value) async {
|
||||||
|
final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
|
||||||
|
if (workspaces != null) {
|
||||||
|
final apps = workspaces.workspace.apps.items;
|
||||||
|
for (var app in apps) {
|
||||||
|
final views = await getViews(appId: app.id).then(
|
||||||
|
(value) => value
|
||||||
|
.getLeftOrNull<List<ViewPB>>()
|
||||||
|
?.where((e) => e.layout == layoutType)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
if (views != null && views.isNotEmpty) {
|
||||||
|
result.add(Tuple2(app, views));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Either<ViewPB, FlowyError>> getView(
|
||||||
|
String appID,
|
||||||
|
String viewID,
|
||||||
|
) async {
|
||||||
|
final payload = AppIdPB.create()..value = appID;
|
||||||
|
return FolderEventReadApp(payload).send().then((result) {
|
||||||
|
return result.fold(
|
||||||
|
(app) => left(
|
||||||
|
app.belongings.items.firstWhere((e) => e.id == viewID),
|
||||||
|
),
|
||||||
|
(error) => right(error),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppFlowy on Either {
|
||||||
|
T? getLeftOrNull<T>() {
|
||||||
|
if (isLeft()) {
|
||||||
|
final result = fold<T?>((l) => l, (r) => null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "type": "text", "delta": [] },
|
{ "type": "text", "delta": [] },
|
||||||
|
{ "type": "board" },
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"delta": [
|
"delta": [
|
||||||
|
@ -44,3 +44,4 @@ export 'src/plugins/markdown/document_markdown.dart';
|
|||||||
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
||||||
export 'src/commands/text/text_commands.dart';
|
export 'src/commands/text/text_commands.dart';
|
||||||
export 'src/render/toolbar/toolbar_item.dart';
|
export 'src/render/toolbar/toolbar_item.dart';
|
||||||
|
export 'src/extensions/node_extensions.dart';
|
||||||
|
@ -71,7 +71,7 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||||||
Attributes _attributes;
|
Attributes _attributes;
|
||||||
|
|
||||||
// Renderable
|
// Renderable
|
||||||
GlobalKey? key;
|
final key = GlobalKey();
|
||||||
final layerLink = LayerLink();
|
final layerLink = LayerLink();
|
||||||
|
|
||||||
Attributes get attributes => {..._attributes};
|
Attributes get attributes => {..._attributes};
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
|
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension NodeExtensions on Node {
|
extension NodeExtensions on Node {
|
||||||
RenderBox? get renderBox =>
|
RenderBox? get renderBox =>
|
||||||
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
key.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||||
|
|
||||||
BuildContext? get context => key?.currentContext;
|
BuildContext? get context => key.currentContext;
|
||||||
SelectableMixin? get selectable =>
|
SelectableMixin? get selectable =>
|
||||||
key?.currentState?.unwrapOrNull<SelectableMixin>();
|
key.currentState?.unwrapOrNull<SelectableMixin>();
|
||||||
|
|
||||||
bool inSelection(Selection selection) {
|
bool inSelection(Selection selection) {
|
||||||
if (selection.start.path <= selection.end.path) {
|
if (selection.start.path <= selection.end.path) {
|
||||||
@ -28,4 +29,11 @@ extension NodeExtensions on Node {
|
|||||||
}
|
}
|
||||||
return Rect.zero;
|
return Rect.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSelected(EditorState editorState) {
|
||||||
|
final currentSelectedNodes =
|
||||||
|
editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
return currentSelectedNodes.length == 1 &&
|
||||||
|
currentSelectedNodes.first == this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
ShortcutEventHandler cursorLeftSelect = (editorState, event) {
|
ShortcutEventHandler cursorLeftSelect = (editorState, event) {
|
||||||
|
@ -74,8 +74,6 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService {
|
|||||||
node.subtype == null ? node.type : '${node.type}/${node.subtype!}';
|
node.subtype == null ? node.type : '${node.type}/${node.subtype!}';
|
||||||
final builder = _builders[name];
|
final builder = _builders[name];
|
||||||
if (builder != null && builder.nodeValidator(node)) {
|
if (builder != null && builder.nodeValidator(node)) {
|
||||||
final key = GlobalKey(debugLabel: name);
|
|
||||||
node.key = key;
|
|
||||||
return _autoUpdateNodeWidget(builder, context);
|
return _autoUpdateNodeWidget(builder, context);
|
||||||
} else {
|
} else {
|
||||||
// Returns a SizeBox with 0 height if no builder found.
|
// Returns a SizeBox with 0 height if no builder found.
|
||||||
|
@ -1,28 +1,18 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
|
||||||
|
|
||||||
class MockNode extends Mock implements Node {}
|
class MockNode extends Mock implements Node {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final mockNode = MockNode();
|
|
||||||
|
|
||||||
group('NodeExtensions::', () {
|
group('NodeExtensions::', () {
|
||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: Position(path: [0]),
|
start: Position(path: [0]),
|
||||||
end: Position(path: [1]),
|
end: Position(path: [1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
test('rect - renderBox is null', () {
|
|
||||||
when(mockNode.renderBox).thenReturn(null);
|
|
||||||
final result = mockNode.rect;
|
|
||||||
expect(result, Rect.zero);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('inSelection', () {
|
test('inSelection', () {
|
||||||
// I use an empty implementation instead of mock, because the mocked
|
// I use an empty implementation instead of mock, because the mocked
|
||||||
// version throws error trying to access the path.
|
// version throws error trying to access the path.
|
||||||
|
@ -43,7 +43,7 @@ void main() async {
|
|||||||
final selection =
|
final selection =
|
||||||
Selection.single(path: [0], startOffset: 0, endOffset: text.length);
|
Selection.single(path: [0], startOffset: 0, endOffset: text.length);
|
||||||
var node = editor.nodeAtPath([0]) as TextNode;
|
var node = editor.nodeAtPath([0]) as TextNode;
|
||||||
var state = node.key?.currentState as DefaultSelectable;
|
var state = node.key.currentState as DefaultSelectable;
|
||||||
var checkboxWidget = find.byKey(state.iconKey!);
|
var checkboxWidget = find.byKey(state.iconKey!);
|
||||||
await tester.tap(checkboxWidget);
|
await tester.tap(checkboxWidget);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@ -56,7 +56,7 @@ void main() async {
|
|||||||
expect(node.allSatisfyStrikethroughInSelection(selection), true);
|
expect(node.allSatisfyStrikethroughInSelection(selection), true);
|
||||||
|
|
||||||
node = editor.nodeAtPath([0]) as TextNode;
|
node = editor.nodeAtPath([0]) as TextNode;
|
||||||
state = node.key?.currentState as DefaultSelectable;
|
state = node.key.currentState as DefaultSelectable;
|
||||||
await tester.ensureVisible(find.byKey(state.iconKey!));
|
await tester.ensureVisible(find.byKey(state.iconKey!));
|
||||||
await tester.tap(find.byKey(state.iconKey!));
|
await tester.tap(find.byKey(state.iconKey!));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
@ -21,7 +21,7 @@ void main() async {
|
|||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
|
|
||||||
final secondTextNode = editor.nodeAtPath([1]);
|
final secondTextNode = editor.nodeAtPath([1]);
|
||||||
final finder = find.byKey(secondTextNode!.key!);
|
final finder = find.byKey(secondTextNode!.key);
|
||||||
|
|
||||||
final rect = tester.getRect(finder);
|
final rect = tester.getRect(finder);
|
||||||
// tap at the beginning
|
// tap at the beginning
|
||||||
@ -48,7 +48,7 @@ void main() async {
|
|||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
|
|
||||||
final secondTextNode = editor.nodeAtPath([1]);
|
final secondTextNode = editor.nodeAtPath([1]);
|
||||||
final finder = find.byKey(secondTextNode!.key!);
|
final finder = find.byKey(secondTextNode!.key);
|
||||||
|
|
||||||
final rect = tester.getRect(finder);
|
final rect = tester.getRect(finder);
|
||||||
// double tap
|
// double tap
|
||||||
@ -70,7 +70,7 @@ void main() async {
|
|||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
|
|
||||||
final secondTextNode = editor.nodeAtPath([1]);
|
final secondTextNode = editor.nodeAtPath([1]);
|
||||||
final finder = find.byKey(secondTextNode!.key!);
|
final finder = find.byKey(secondTextNode!.key);
|
||||||
|
|
||||||
final rect = tester.getRect(finder);
|
final rect = tester.getRect(finder);
|
||||||
// triple tap
|
// triple tap
|
||||||
@ -93,7 +93,7 @@ void main() async {
|
|||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
|
|
||||||
final secondTextNode = editor.nodeAtPath([1]) as TextNode;
|
final secondTextNode = editor.nodeAtPath([1]) as TextNode;
|
||||||
final finder = find.byKey(secondTextNode.key!);
|
final finder = find.byKey(secondTextNode.key);
|
||||||
|
|
||||||
final rect = tester.getRect(finder);
|
final rect = tester.getRect(finder);
|
||||||
// secondary tap
|
// secondary tap
|
||||||
|
@ -47,7 +47,7 @@ SelectionMenuItem mathEquationMenuItem = SelectionMenuItem(
|
|||||||
final mathEquationState = editorState.document
|
final mathEquationState = editorState.document
|
||||||
.nodeAtPath(mathEquationNodePath)
|
.nodeAtPath(mathEquationNodePath)
|
||||||
?.key
|
?.key
|
||||||
?.currentState;
|
.currentState;
|
||||||
if (mathEquationState != null &&
|
if (mathEquationState != null &&
|
||||||
mathEquationState is _MathEquationNodeWidgetState) {
|
mathEquationState is _MathEquationNodeWidgetState) {
|
||||||
mathEquationState.showEditingDialog();
|
mathEquationState.showEditingDialog();
|
||||||
|
@ -16,7 +16,7 @@ dependencies:
|
|||||||
path: ../appflowy_editor
|
path: ../appflowy_editor
|
||||||
flowy_infra:
|
flowy_infra:
|
||||||
path: ../flowy_infra
|
path: ../flowy_infra
|
||||||
flowy_infra_ui:
|
flowy_infra_ui:
|
||||||
path: ../flowy_infra_ui
|
path: ../flowy_infra_ui
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: ../appflowy_popover
|
path: ../appflowy_popover
|
||||||
|
Loading…
Reference in New Issue
Block a user