doc_bloc.dart 6.1 KB

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