document_page.dart 7.6 KB

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