doc_bloc.dart 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import 'dart:async';
  2. import 'package:appflowy/plugins/document/application/doc_service.dart';
  3. import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
  4. import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
  5. import 'package:appflowy/plugins/trash/application/trash_service.dart';
  6. import 'package:appflowy/user/application/user_service.dart';
  7. import 'package:appflowy/workspace/application/doc/doc_listener.dart';
  8. import 'package:appflowy/workspace/application/view/view_listener.dart';
  9. import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  12. import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
  13. import 'package:appflowy_editor/appflowy_editor.dart'
  14. show
  15. EditorState,
  16. LogLevel,
  17. TransactionTime,
  18. Selection,
  19. Position,
  20. paragraphNode;
  21. import 'package:dartz/dartz.dart';
  22. import 'package:flutter/foundation.dart';
  23. import 'package:flutter_bloc/flutter_bloc.dart';
  24. import 'package:freezed_annotation/freezed_annotation.dart';
  25. part 'doc_bloc.freezed.dart';
  26. class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
  27. DocumentBloc({
  28. required this.view,
  29. }) : _documentListener = DocumentListener(id: view.id),
  30. _viewListener = ViewListener(viewId: view.id),
  31. _documentService = DocumentService(),
  32. _trashService = TrashService(),
  33. super(DocumentState.initial()) {
  34. _transactionAdapter = TransactionAdapter(
  35. documentId: view.id,
  36. documentService: _documentService,
  37. );
  38. on<DocumentEvent>(_onDocumentEvent);
  39. }
  40. final ViewPB view;
  41. final DocumentListener _documentListener;
  42. final ViewListener _viewListener;
  43. final DocumentService _documentService;
  44. final TrashService _trashService;
  45. late final TransactionAdapter _transactionAdapter;
  46. EditorState? editorState;
  47. StreamSubscription? _subscription;
  48. @override
  49. Future<void> close() async {
  50. await _viewListener.stop();
  51. await _subscription?.cancel();
  52. await _documentService.closeDocument(view: view);
  53. editorState?.cancelSubscription();
  54. return super.close();
  55. }
  56. Future<void> _onDocumentEvent(
  57. DocumentEvent event,
  58. Emitter<DocumentState> emit,
  59. ) async {
  60. await event.map(
  61. initial: (Initial value) async {
  62. final state = await _fetchDocumentState();
  63. await _subscribe(state);
  64. emit(state);
  65. },
  66. moveToTrash: (MoveToTrash value) async {
  67. emit(state.copyWith(isDeleted: true));
  68. },
  69. restore: (Restore value) async {
  70. emit(state.copyWith(isDeleted: false));
  71. },
  72. deletePermanently: (DeletePermanently value) async {
  73. final result = await _trashService.deleteViews([view.id]);
  74. final forceClose = result.fold((l) => true, (r) => false);
  75. emit(state.copyWith(forceClose: forceClose));
  76. },
  77. restorePage: (RestorePage value) async {
  78. final result = await _trashService.putback(view.id);
  79. final isDeleted = result.fold((l) => false, (r) => true);
  80. emit(state.copyWith(isDeleted: isDeleted));
  81. },
  82. );
  83. }
  84. Future<void> _subscribe(DocumentState state) async {
  85. _onViewChanged();
  86. _onDocumentChanged();
  87. // create the editor state
  88. await state.loadingState.whenOrNull(
  89. finish: (data) async => data.map((r) {
  90. _initAppFlowyEditorState(r);
  91. }),
  92. );
  93. }
  94. /// subscribe to the view(document page) change
  95. void _onViewChanged() {
  96. _viewListener.start(
  97. onViewMoveToTrash: (r) {
  98. r.swap().map((r) => add(const DocumentEvent.moveToTrash()));
  99. },
  100. onViewDeleted: (r) {
  101. r.swap().map((r) => add(const DocumentEvent.moveToTrash()));
  102. },
  103. onViewRestored: (r) =>
  104. r.swap().map((r) => add(const DocumentEvent.restore())),
  105. );
  106. }
  107. /// subscribe to the document content change
  108. void _onDocumentChanged() {
  109. _documentListener.start(
  110. didReceiveUpdate: syncDocumentDataPB,
  111. );
  112. }
  113. /// Fetch document
  114. Future<DocumentState> _fetchDocumentState() async {
  115. final result = await UserBackendService.getCurrentUserProfile().then(
  116. (value) async => value.andThen(
  117. // open the document
  118. await _documentService.openDocument(view: view),
  119. ),
  120. );
  121. return state.copyWith(
  122. loadingState: DocumentLoadingState.finish(result),
  123. );
  124. }
  125. Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
  126. final document = data.toDocument();
  127. if (document == null) {
  128. assert(false, 'document is null');
  129. return;
  130. }
  131. final editorState = EditorState(document: document);
  132. this.editorState = editorState;
  133. // subscribe to the document change from the editor
  134. _subscription = editorState.transactionStream.listen((event) async {
  135. final time = event.$1;
  136. if (time != TransactionTime.before) {
  137. return;
  138. }
  139. await _transactionAdapter.apply(event.$2, editorState);
  140. // check if the document is empty.
  141. applyRules();
  142. });
  143. // output the log from the editor when debug mode
  144. if (kDebugMode) {
  145. editorState.logConfiguration
  146. ..level = LogLevel.all
  147. ..handler = (log) {
  148. // Log.debug(log);
  149. };
  150. }
  151. }
  152. Future<void> applyRules() async {
  153. ensureAtLeastOneParagraphExists();
  154. ensureLastNodeIsEditable();
  155. }
  156. Future<void> ensureLastNodeIsEditable() async {
  157. final editorState = this.editorState;
  158. if (editorState == null) {
  159. return;
  160. }
  161. final document = editorState.document;
  162. final lastNode = document.root.children.lastOrNull;
  163. if (lastNode == null || lastNode.delta == null) {
  164. final transaction = editorState.transaction;
  165. transaction.insertNode([document.root.children.length], paragraphNode());
  166. transaction.afterSelection = transaction.beforeSelection;
  167. await editorState.apply(transaction);
  168. }
  169. }
  170. Future<void> ensureAtLeastOneParagraphExists() async {
  171. final editorState = this.editorState;
  172. if (editorState == null) {
  173. return;
  174. }
  175. final document = editorState.document;
  176. if (document.root.children.isEmpty) {
  177. final transaction = editorState.transaction;
  178. transaction.insertNode([0], paragraphNode());
  179. transaction.afterSelection = Selection.collapsed(
  180. Position(path: [0], offset: 0),
  181. );
  182. await editorState.apply(transaction);
  183. }
  184. }
  185. void syncDocumentDataPB(DocEventPB docEvent) {
  186. // prettyPrintJson(docEvent.toProto3Json());
  187. // todo: integrate the document change to the editor
  188. // for (final event in docEvent.events) {
  189. // for (final blockEvent in event.event) {
  190. // switch (blockEvent.command) {
  191. // case DeltaTypePB.Inserted:
  192. // break;
  193. // case DeltaTypePB.Updated:
  194. // break;
  195. // case DeltaTypePB.Removed:
  196. // break;
  197. // default:
  198. // }
  199. // }
  200. // }
  201. }
  202. }
  203. @freezed
  204. class DocumentEvent with _$DocumentEvent {
  205. const factory DocumentEvent.initial() = Initial;
  206. const factory DocumentEvent.moveToTrash() = MoveToTrash;
  207. const factory DocumentEvent.restore() = Restore;
  208. const factory DocumentEvent.restorePage() = RestorePage;
  209. const factory DocumentEvent.deletePermanently() = DeletePermanently;
  210. }
  211. @freezed
  212. class DocumentState with _$DocumentState {
  213. const factory DocumentState({
  214. required DocumentLoadingState loadingState,
  215. required bool isDeleted,
  216. required bool forceClose,
  217. UserProfilePB? userProfilePB,
  218. }) = _DocumentState;
  219. factory DocumentState.initial() => const DocumentState(
  220. loadingState: _Loading(),
  221. isDeleted: false,
  222. forceClose: false,
  223. userProfilePB: null,
  224. );
  225. }
  226. @freezed
  227. class DocumentLoadingState with _$DocumentLoadingState {
  228. const factory DocumentLoadingState.loading() = _Loading;
  229. const factory DocumentLoadingState.finish(
  230. Either<FlowyError, DocumentDataPB> successOrFail,
  231. ) = _Finish;
  232. }