doc_bloc.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import 'dart:convert';
  2. import 'package:appflowy/plugins/trash/application/trash_service.dart';
  3. import 'package:appflowy/user/application/user_service.dart';
  4. import 'package:appflowy/workspace/application/view/view_listener.dart';
  5. import 'package:appflowy/plugins/document/application/doc_service.dart';
  6. import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
  7. import 'package:appflowy_editor/appflowy_editor.dart'
  8. show EditorState, Document, Transaction;
  9. import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
  12. import 'package:appflowy_backend/log.dart';
  13. import 'package:flutter_bloc/flutter_bloc.dart';
  14. import 'package:freezed_annotation/freezed_annotation.dart';
  15. import 'package:dartz/dartz.dart';
  16. import 'dart:async';
  17. import 'package:appflowy/util/either_extension.dart';
  18. part 'doc_bloc.freezed.dart';
  19. class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
  20. final ViewPB view;
  21. final DocumentService _documentService;
  22. final ViewListener _listener;
  23. final TrashService _trashService;
  24. EditorState? editorState;
  25. StreamSubscription? _subscription;
  26. DocumentBloc({
  27. required this.view,
  28. }) : _documentService = DocumentService(),
  29. _listener = ViewListener(view: view),
  30. _trashService = TrashService(),
  31. super(DocumentState.initial()) {
  32. on<DocumentEvent>((event, emit) async {
  33. await event.map(
  34. initial: (Initial value) async {
  35. await _initial(value, emit);
  36. _listenOnViewChange();
  37. },
  38. deleted: (Deleted value) async {
  39. emit(state.copyWith(isDeleted: true));
  40. },
  41. restore: (Restore value) async {
  42. emit(state.copyWith(isDeleted: false));
  43. },
  44. deletePermanently: (DeletePermanently value) async {
  45. final result = await _trashService
  46. .deleteViews([Tuple2(view.id, TrashType.TrashView)]);
  47. final newState = result.fold(
  48. (l) => state.copyWith(forceClose: true), (r) => state);
  49. emit(newState);
  50. },
  51. restorePage: (RestorePage value) async {
  52. final result = await _trashService.putback(view.id);
  53. final newState = result.fold(
  54. (l) => state.copyWith(isDeleted: false), (r) => state);
  55. emit(newState);
  56. },
  57. );
  58. });
  59. }
  60. @override
  61. Future<void> close() async {
  62. await _listener.stop();
  63. if (_subscription != null) {
  64. await _subscription?.cancel();
  65. }
  66. await _documentService.closeDocument(docId: view.id);
  67. return super.close();
  68. }
  69. Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
  70. final userProfile = await UserBackendService.getCurrentUserProfile();
  71. if (userProfile.isRight()) {
  72. emit(
  73. state.copyWith(
  74. loadingState: DocumentLoadingState.finish(
  75. right(userProfile.asRight()),
  76. ),
  77. ),
  78. );
  79. return;
  80. }
  81. final result = await _documentService.openDocument(view: view);
  82. result.fold(
  83. (documentData) {
  84. final document = Document.fromJson(jsonDecode(documentData.content));
  85. editorState = EditorState(document: document);
  86. _listenOnDocumentChange();
  87. emit(
  88. state.copyWith(
  89. loadingState: DocumentLoadingState.finish(left(unit)),
  90. userProfilePB: userProfile.asLeft(),
  91. ),
  92. );
  93. },
  94. (err) {
  95. emit(
  96. state.copyWith(
  97. loadingState: DocumentLoadingState.finish(right(err)),
  98. ),
  99. );
  100. },
  101. );
  102. }
  103. void _listenOnViewChange() {
  104. _listener.start(
  105. onViewDeleted: (result) {
  106. result.fold(
  107. (view) => add(const DocumentEvent.deleted()),
  108. (error) {},
  109. );
  110. },
  111. onViewRestored: (result) {
  112. result.fold(
  113. (view) => add(const DocumentEvent.restore()),
  114. (error) {},
  115. );
  116. },
  117. );
  118. }
  119. void _listenOnDocumentChange() {
  120. _subscription = editorState?.transactionStream.listen((transaction) {
  121. final json = jsonEncode(TransactionAdaptor(transaction).toJson());
  122. _documentService
  123. .applyEdit(docId: view.id, operations: json)
  124. .then((result) {
  125. result.fold(
  126. (l) => null,
  127. (err) => Log.error(err),
  128. );
  129. });
  130. });
  131. }
  132. }
  133. @freezed
  134. class DocumentEvent with _$DocumentEvent {
  135. const factory DocumentEvent.initial() = Initial;
  136. const factory DocumentEvent.deleted() = Deleted;
  137. const factory DocumentEvent.restore() = Restore;
  138. const factory DocumentEvent.restorePage() = RestorePage;
  139. const factory DocumentEvent.deletePermanently() = DeletePermanently;
  140. }
  141. @freezed
  142. class DocumentState with _$DocumentState {
  143. const factory DocumentState({
  144. required DocumentLoadingState loadingState,
  145. required bool isDeleted,
  146. required bool forceClose,
  147. UserProfilePB? userProfilePB,
  148. }) = _DocumentState;
  149. factory DocumentState.initial() => const DocumentState(
  150. loadingState: _Loading(),
  151. isDeleted: false,
  152. forceClose: false,
  153. userProfilePB: null,
  154. );
  155. }
  156. @freezed
  157. class DocumentLoadingState with _$DocumentLoadingState {
  158. const factory DocumentLoadingState.loading() = _Loading;
  159. const factory DocumentLoadingState.finish(
  160. Either<Unit, FlowyError> successOrFail) = _Finish;
  161. }
  162. /// Uses to erase the different between appflowy editor and the backend
  163. class TransactionAdaptor {
  164. final Transaction transaction;
  165. TransactionAdaptor(this.transaction);
  166. Map<String, dynamic> toJson() {
  167. final json = <String, dynamic>{};
  168. if (transaction.operations.isNotEmpty) {
  169. // The backend uses [0,0] as the beginning path, but the editor uses [0].
  170. // So it needs to extend the path by inserting `0` at the head for all
  171. // operations before passing to the backend.
  172. json['operations'] = transaction.operations
  173. .map((e) => e.copyWith(path: [0, ...e.path]).toJson())
  174. .toList();
  175. }
  176. if (transaction.afterSelection != null) {
  177. final selection = transaction.afterSelection!;
  178. final start = selection.start;
  179. final end = selection.end;
  180. json['after_selection'] = selection
  181. .copyWith(
  182. start: start.copyWith(path: [0, ...start.path]),
  183. end: end.copyWith(path: [0, ...end.path]),
  184. )
  185. .toJson();
  186. }
  187. if (transaction.beforeSelection != null) {
  188. final selection = transaction.beforeSelection!;
  189. final start = selection.start;
  190. final end = selection.end;
  191. json['before_selection'] = selection
  192. .copyWith(
  193. start: start.copyWith(path: [0, ...start.path]),
  194. end: end.copyWith(path: [0, ...end.path]),
  195. )
  196. .toJson();
  197. }
  198. return json;
  199. }
  200. }