document_page.dart 7.0 KB

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