document_page.dart 6.8 KB

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