document_page.dart 7.7 KB

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