document_page.dart 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart';
  2. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  3. import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
  4. import 'package:appflowy_editor/appflowy_editor.dart';
  5. import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
  6. import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
  7. import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
  8. import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
  9. import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
  10. import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
  11. import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
  12. import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart';
  13. import 'package:dartz/dartz.dart' as dartz;
  14. import 'package:flowy_infra_ui/widget/error_page.dart';
  15. import 'package:flutter_bloc/flutter_bloc.dart';
  16. import 'package:flutter/material.dart';
  17. import 'package:intl/intl.dart';
  18. import '../../startup/startup.dart';
  19. import 'application/doc_bloc.dart';
  20. import 'editor_styles.dart';
  21. import 'presentation/banner.dart';
  22. import 'presentation/plugins/grid/grid_view_menu_item.dart';
  23. import 'presentation/plugins/board/board_menu_item.dart';
  24. class DocumentPage extends StatefulWidget {
  25. final VoidCallback onDeleted;
  26. final ViewPB view;
  27. DocumentPage({
  28. required this.view,
  29. required this.onDeleted,
  30. Key? key,
  31. }) : super(key: ValueKey(view.id));
  32. @override
  33. State<DocumentPage> createState() => _DocumentPageState();
  34. }
  35. class _DocumentPageState extends State<DocumentPage> {
  36. late DocumentBloc documentBloc;
  37. @override
  38. void initState() {
  39. // The appflowy editor use Intl as localization, set the default language as fallback.
  40. Intl.defaultLocale = 'en_US';
  41. documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
  42. ..add(const DocumentEvent.initial());
  43. super.initState();
  44. }
  45. @override
  46. void dispose() {
  47. documentBloc.close();
  48. super.dispose();
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return MultiBlocProvider(
  53. providers: [
  54. BlocProvider<DocumentBloc>.value(value: documentBloc),
  55. ],
  56. child: BlocBuilder<DocumentBloc, DocumentState>(
  57. builder: (context, state) {
  58. return state.loadingState.map(
  59. loading: (_) => SizedBox.expand(
  60. child: Container(color: Colors.transparent),
  61. ),
  62. finish: (result) => result.successOrFail.fold(
  63. (_) {
  64. if (state.forceClose) {
  65. widget.onDeleted();
  66. return const SizedBox();
  67. } else if (documentBloc.editorState == null) {
  68. return const SizedBox();
  69. } else {
  70. return _renderDocument(context, state);
  71. }
  72. },
  73. (err) => FlowyErrorPage(err.toString()),
  74. ),
  75. );
  76. },
  77. ),
  78. );
  79. }
  80. Widget _renderDocument(BuildContext context, DocumentState state) {
  81. return Column(
  82. children: [
  83. if (state.isDeleted) _renderBanner(context),
  84. // AppFlowy Editor
  85. const _AppFlowyEditorPage(),
  86. ],
  87. );
  88. }
  89. Widget _renderBanner(BuildContext context) {
  90. return DocumentBanner(
  91. onRestore: () =>
  92. context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
  93. onDelete: () => context
  94. .read<DocumentBloc>()
  95. .add(const DocumentEvent.deletePermanently()),
  96. );
  97. }
  98. }
  99. class _AppFlowyEditorPage extends StatefulWidget {
  100. const _AppFlowyEditorPage({
  101. Key? key,
  102. }) : super(key: key);
  103. @override
  104. State<_AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
  105. }
  106. class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
  107. late DocumentBloc documentBloc;
  108. late EditorState editorState;
  109. String? get openAIKey => documentBloc.state.userProfilePB?.openaiKey;
  110. @override
  111. void initState() {
  112. super.initState();
  113. documentBloc = context.read<DocumentBloc>();
  114. editorState = documentBloc.editorState ?? EditorState.empty();
  115. }
  116. @override
  117. Widget build(BuildContext context) {
  118. final theme = Theme.of(context);
  119. final autoFocusParameters = _autoFocusParameters();
  120. final editor = AppFlowyEditor(
  121. editorState: editorState,
  122. autoFocus: autoFocusParameters.value1,
  123. focusedSelection: autoFocusParameters.value2,
  124. customBuilders: {
  125. // Divider
  126. kDividerType: DividerWidgetBuilder(),
  127. // Math Equation
  128. kMathEquationType: MathEquationNodeWidgetBuidler(),
  129. // Code Block
  130. kCodeBlockType: CodeBlockNodeWidgetBuilder(),
  131. // Board
  132. kBoardType: BoardNodeWidgetBuilder(),
  133. // Grid
  134. kGridType: GridNodeWidgetBuilder(),
  135. // Card
  136. kCalloutType: CalloutNodeWidgetBuilder(),
  137. // Auto Generator,
  138. kAutoCompletionInputType: AutoCompletionInputBuilder(),
  139. // Cover
  140. kCoverType: CoverNodeWidgetBuilder(),
  141. // Smart Edit,
  142. kSmartEditType: SmartEditInputBuilder(),
  143. },
  144. shortcutEvents: [
  145. // Divider
  146. insertDividerEvent,
  147. // Code Block
  148. enterInCodeBlock,
  149. ignoreKeysInCodeBlock,
  150. pasteInCodeBlock,
  151. ],
  152. selectionMenuItems: [
  153. // Divider
  154. dividerMenuItem,
  155. // Math Equation
  156. mathEquationMenuItem,
  157. // Code Block
  158. codeBlockMenuItem,
  159. // Emoji
  160. emojiMenuItem,
  161. // Board
  162. boardMenuItem,
  163. // Create Board
  164. boardViewMenuItem(documentBloc),
  165. // Grid
  166. gridMenuItem,
  167. // Create Grid
  168. gridViewMenuItem(documentBloc),
  169. // Callout
  170. calloutMenuItem,
  171. // AI
  172. // enable open ai features if needed.
  173. if (openAIKey != null && openAIKey!.isNotEmpty) ...[
  174. autoGeneratorMenuItem,
  175. ],
  176. ],
  177. toolbarItems: [
  178. smartEditItem,
  179. ],
  180. themeData: theme.copyWith(
  181. extensions: [
  182. ...theme.extensions.values,
  183. customEditorTheme(context),
  184. ...customPluginTheme(context),
  185. ],
  186. ),
  187. );
  188. return Expanded(
  189. child: Center(
  190. child: Container(
  191. constraints: const BoxConstraints(
  192. maxWidth: double.infinity,
  193. ),
  194. child: editor,
  195. ),
  196. ),
  197. );
  198. }
  199. @override
  200. void dispose() {
  201. _clearTemporaryNodes();
  202. super.dispose();
  203. }
  204. Future<void> _clearTemporaryNodes() async {
  205. final document = editorState.document;
  206. if (document.root.children.isEmpty) {
  207. return;
  208. }
  209. final temporaryNodeTypes = [
  210. kAutoCompletionInputType,
  211. kSmartEditType,
  212. ];
  213. final iterator = NodeIterator(
  214. document: document,
  215. startNode: document.root.children.first,
  216. );
  217. final transaction = editorState.transaction;
  218. while (iterator.moveNext()) {
  219. final node = iterator.current;
  220. if (temporaryNodeTypes.contains(node.type)) {
  221. transaction.deleteNode(node);
  222. }
  223. }
  224. if (transaction.operations.isNotEmpty) {
  225. await editorState.apply(transaction, withUpdateCursor: false);
  226. }
  227. }
  228. dartz.Tuple2<bool, Selection?> _autoFocusParameters() {
  229. if (editorState.document.isEmpty) {
  230. return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
  231. }
  232. final texts = editorState.document.root.children.whereType<TextNode>();
  233. if (texts.every((element) => element.toPlainText().isEmpty)) {
  234. return dartz.Tuple2(
  235. true,
  236. Selection.single(path: texts.first.path, startOffset: 0),
  237. );
  238. }
  239. return const dartz.Tuple2(false, null);
  240. }
  241. }