doc_bloc.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 service;
  19. final ViewListener listener;
  20. final TrashService trashService;
  21. late EditorState editorState;
  22. StreamSubscription? _subscription;
  23. DocumentBloc({
  24. required this.view,
  25. required this.service,
  26. required this.listener,
  27. required this.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 service.closeDocument(docId: view.id);
  64. return super.close();
  65. }
  66. Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
  67. final result = await service.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. service.applyEdit(docId: view.id, operations: json).then((result) {
  108. result.fold(
  109. (l) => null,
  110. (err) => Log.error(err),
  111. );
  112. });
  113. });
  114. }
  115. }
  116. @freezed
  117. class DocumentEvent with _$DocumentEvent {
  118. const factory DocumentEvent.initial() = Initial;
  119. const factory DocumentEvent.deleted() = Deleted;
  120. const factory DocumentEvent.restore() = Restore;
  121. const factory DocumentEvent.restorePage() = RestorePage;
  122. const factory DocumentEvent.deletePermanently() = DeletePermanently;
  123. }
  124. @freezed
  125. class DocumentState with _$DocumentState {
  126. const factory DocumentState({
  127. required DocumentLoadingState loadingState,
  128. required bool isDeleted,
  129. required bool forceClose,
  130. }) = _DocumentState;
  131. factory DocumentState.initial() => const DocumentState(
  132. loadingState: _Loading(),
  133. isDeleted: false,
  134. forceClose: false,
  135. );
  136. }
  137. @freezed
  138. class DocumentLoadingState with _$DocumentLoadingState {
  139. const factory DocumentLoadingState.loading() = _Loading;
  140. const factory DocumentLoadingState.finish(
  141. Either<Unit, FlowyError> successOrFail) = _Finish;
  142. }
  143. /// Uses to erase the different between appflowy editor and the backend
  144. class TransactionAdaptor {
  145. final Transaction transaction;
  146. TransactionAdaptor(this.transaction);
  147. Map<String, dynamic> toJson() {
  148. final json = <String, dynamic>{};
  149. if (transaction.operations.isNotEmpty) {
  150. // The backend uses [0,0] as the beginning path, but the editor uses [0].
  151. // So it needs to extend the path by inserting `0` at the head for all
  152. // operations before passing to the backend.
  153. json['operations'] = transaction.operations
  154. .map((e) => e.copyWith(path: [0, ...e.path]).toJson())
  155. .toList();
  156. }
  157. if (transaction.afterSelection != null) {
  158. final selection = transaction.afterSelection!;
  159. final start = selection.start;
  160. final end = selection.end;
  161. json['after_selection'] = selection
  162. .copyWith(
  163. start: start.copyWith(path: [0, ...start.path]),
  164. end: end.copyWith(path: [0, ...end.path]),
  165. )
  166. .toJson();
  167. }
  168. if (transaction.beforeSelection != null) {
  169. final selection = transaction.beforeSelection!;
  170. final start = selection.start;
  171. final end = selection.end;
  172. json['before_selection'] = selection
  173. .copyWith(
  174. start: start.copyWith(path: [0, ...start.path]),
  175. end: end.copyWith(path: [0, ...end.path]),
  176. )
  177. .toJson();
  178. }
  179. return json;
  180. }
  181. }