| 
									
										
										
										
											2023-08-17 14:50:48 +08:00
										 |  |  | import 'dart:async'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import 'package:appflowy/plugins/document/application/doc_service.dart'; | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  | import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; | 
					
						
							|  |  |  | import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; | 
					
						
							| 
									
										
										
										
											2023-02-26 16:27:17 +08:00
										 |  |  | import 'package:appflowy/plugins/trash/application/trash_service.dart'; | 
					
						
							|  |  |  | import 'package:appflowy/user/application/user_service.dart'; | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  | import 'package:appflowy/util/json_print.dart'; | 
					
						
							| 
									
										
										
										
											2023-04-13 18:53:51 +08:00
										 |  |  | import 'package:appflowy/workspace/application/doc/doc_listener.dart'; | 
					
						
							| 
									
										
										
										
											2023-08-17 14:50:48 +08:00
										 |  |  | import 'package:appflowy/workspace/application/view/view_listener.dart'; | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  | import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; | 
					
						
							| 
									
										
										
										
											2023-01-08 12:10:53 +08:00
										 |  |  | import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; | 
					
						
							| 
									
										
										
										
											2023-04-04 08:41:16 +08:00
										 |  |  | import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; | 
					
						
							| 
									
										
										
										
											2023-08-17 14:50:48 +08:00
										 |  |  | import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; | 
					
						
							|  |  |  | import 'package:appflowy_editor/appflowy_editor.dart' | 
					
						
							|  |  |  |     show | 
					
						
							|  |  |  |         EditorState, | 
					
						
							|  |  |  |         LogLevel, | 
					
						
							|  |  |  |         TransactionTime, | 
					
						
							|  |  |  |         Selection, | 
					
						
							|  |  |  |         Position, | 
					
						
							|  |  |  |         paragraphNode; | 
					
						
							|  |  |  | import 'package:dartz/dartz.dart'; | 
					
						
							| 
									
										
										
										
											2023-03-07 09:33:59 +08:00
										 |  |  | import 'package:flutter/foundation.dart'; | 
					
						
							| 
									
										
										
										
											2021-09-11 21:30:58 +08:00
										 |  |  | import 'package:flutter_bloc/flutter_bloc.dart'; | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  | import 'package:freezed_annotation/freezed_annotation.dart'; | 
					
						
							| 
									
										
										
										
											2023-08-17 14:50:48 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  | part 'doc_bloc.freezed.dart'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  | class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { | 
					
						
							|  |  |  |   DocumentBloc({ | 
					
						
							| 
									
										
										
										
											2021-10-31 19:48:20 +08:00
										 |  |  |     required this.view, | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   })  : _documentListener = DocumentListener(id: view.id), | 
					
						
							| 
									
										
										
										
											2023-06-14 22:16:33 +08:00
										 |  |  |         _viewListener = ViewListener(viewId: view.id), | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |         _documentService = DocumentService(), | 
					
						
							| 
									
										
										
										
											2022-10-26 10:38:57 +08:00
										 |  |  |         _trashService = TrashService(), | 
					
						
							|  |  |  |         super(DocumentState.initial()) { | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |     _transactionAdapter = TransactionAdapter( | 
					
						
							|  |  |  |       documentId: view.id, | 
					
						
							|  |  |  |       documentService: _documentService, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     on<DocumentEvent>(_onDocumentEvent); | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-09-11 21:30:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   final ViewPB view; | 
					
						
							| 
									
										
										
										
											2021-11-03 22:04:45 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   final DocumentListener _documentListener; | 
					
						
							|  |  |  |   final ViewListener _viewListener; | 
					
						
							| 
									
										
										
										
											2021-11-03 23:03:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   final DocumentService _documentService; | 
					
						
							|  |  |  |   final TrashService _trashService; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   late final TransactionAdapter _transactionAdapter; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   EditorState? editorState; | 
					
						
							|  |  |  |   StreamSubscription? _subscription; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Future<void> close() async { | 
					
						
							|  |  |  |     await _viewListener.stop(); | 
					
						
							|  |  |  |     await _subscription?.cancel(); | 
					
						
							|  |  |  |     await _documentService.closeDocument(view: view); | 
					
						
							|  |  |  |     editorState?.cancelSubscription(); | 
					
						
							| 
									
										
										
										
											2021-10-19 13:56:11 +08:00
										 |  |  |     return super.close(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   Future<void> _onDocumentEvent( | 
					
						
							|  |  |  |     DocumentEvent event, | 
					
						
							|  |  |  |     Emitter<DocumentState> emit, | 
					
						
							|  |  |  |   ) async { | 
					
						
							|  |  |  |     await event.map( | 
					
						
							|  |  |  |       initial: (Initial value) async { | 
					
						
							|  |  |  |         final state = await _fetchDocumentState(); | 
					
						
							|  |  |  |         await _subscribe(state); | 
					
						
							|  |  |  |         emit(state); | 
					
						
							| 
									
										
										
										
											2022-10-22 21:57:44 +08:00
										 |  |  |       }, | 
					
						
							| 
									
										
										
										
											2023-06-05 13:10:14 +08:00
										 |  |  |       moveToTrash: (MoveToTrash value) async { | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |         emit(state.copyWith(isDeleted: true)); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       restore: (Restore value) async { | 
					
						
							|  |  |  |         emit(state.copyWith(isDeleted: false)); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       deletePermanently: (DeletePermanently value) async { | 
					
						
							|  |  |  |         final result = await _trashService.deleteViews([view.id]); | 
					
						
							| 
									
										
										
										
											2023-06-05 13:10:14 +08:00
										 |  |  |         final forceClose = result.fold((l) => true, (r) => false); | 
					
						
							|  |  |  |         emit(state.copyWith(forceClose: forceClose)); | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |       }, | 
					
						
							|  |  |  |       restorePage: (RestorePage value) async { | 
					
						
							|  |  |  |         final result = await _trashService.putback(view.id); | 
					
						
							| 
									
										
										
										
											2023-06-05 13:10:14 +08:00
										 |  |  |         final isDeleted = result.fold((l) => false, (r) => true); | 
					
						
							|  |  |  |         emit(state.copyWith(isDeleted: isDeleted)); | 
					
						
							| 
									
										
										
										
											2022-10-22 21:57:44 +08:00
										 |  |  |       }, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   Future<void> _subscribe(DocumentState state) async { | 
					
						
							|  |  |  |     _onViewChanged(); | 
					
						
							|  |  |  |     _onDocumentChanged(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // create the editor state
 | 
					
						
							|  |  |  |     await state.loadingState.whenOrNull( | 
					
						
							|  |  |  |       finish: (data) async => data.map((r) { | 
					
						
							|  |  |  |         _initAppFlowyEditorState(r); | 
					
						
							|  |  |  |       }), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /// subscribe to the view(document page) change
 | 
					
						
							|  |  |  |   void _onViewChanged() { | 
					
						
							|  |  |  |     _viewListener.start( | 
					
						
							| 
									
										
										
										
											2023-06-05 13:10:14 +08:00
										 |  |  |       onViewMoveToTrash: (r) { | 
					
						
							|  |  |  |         r.swap().map((r) => add(const DocumentEvent.moveToTrash())); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       onViewDeleted: (r) { | 
					
						
							|  |  |  |         r.swap().map((r) => add(const DocumentEvent.moveToTrash())); | 
					
						
							|  |  |  |       }, | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |       onViewRestored: (r) => | 
					
						
							|  |  |  |           r.swap().map((r) => add(const DocumentEvent.restore())), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /// subscribe to the document content change
 | 
					
						
							|  |  |  |   void _onDocumentChanged() { | 
					
						
							|  |  |  |     _documentListener.start( | 
					
						
							|  |  |  |       didReceiveUpdate: (docEvent) { | 
					
						
							|  |  |  |         // todo: integrate the document change to the editor
 | 
					
						
							|  |  |  |         // prettyPrintJson(docEvent.toProto3Json());
 | 
					
						
							| 
									
										
										
										
											2022-05-05 21:15:01 +08:00
										 |  |  |       }, | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-10-20 22:19:01 +08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |   /// Fetch document
 | 
					
						
							|  |  |  |   Future<DocumentState> _fetchDocumentState() async { | 
					
						
							|  |  |  |     final result = await UserBackendService.getCurrentUserProfile().then( | 
					
						
							|  |  |  |       (value) async => value.andThen( | 
					
						
							|  |  |  |         // open the document
 | 
					
						
							|  |  |  |         await _documentService.openDocument(view: view), | 
					
						
							|  |  |  |       ), | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     return state.copyWith( | 
					
						
							|  |  |  |       loadingState: DocumentLoadingState.finish(result), | 
					
						
							| 
									
										
										
										
											2023-04-13 18:53:51 +08:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 16:13:12 +08:00
										 |  |  |   Future<void> _initAppFlowyEditorState(DocumentDataPB data) async { | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |     if (kDebugMode) { | 
					
						
							|  |  |  |       prettyPrintJson(data.toProto3Json()); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     final document = data.toDocument(); | 
					
						
							|  |  |  |     if (document == null) { | 
					
						
							|  |  |  |       assert(false, 'document is null'); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 19:55:43 +08:00
										 |  |  |     final editorState = EditorState(document: document); | 
					
						
							| 
									
										
										
										
											2023-03-07 09:33:59 +08:00
										 |  |  |     this.editorState = editorState; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |     // subscribe to the document change from the editor
 | 
					
						
							| 
									
										
										
										
											2023-06-21 19:53:29 +08:00
										 |  |  |     _subscription = editorState.transactionStream.listen((event) async { | 
					
						
							|  |  |  |       final time = event.$1; | 
					
						
							|  |  |  |       if (time != TransactionTime.before) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       await _transactionAdapter.apply(event.$2, editorState); | 
					
						
							| 
									
										
										
										
											2023-07-02 11:46:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // check if the document is empty.
 | 
					
						
							|  |  |  |       applyRules(); | 
					
						
							| 
									
										
										
										
											2022-10-22 21:57:44 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // output the log from the editor when debug mode
 | 
					
						
							| 
									
										
										
										
											2023-03-07 09:33:59 +08:00
										 |  |  |     if (kDebugMode) { | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |       editorState.logConfiguration | 
					
						
							|  |  |  |         ..level = LogLevel.all | 
					
						
							|  |  |  |         ..handler = (log) { | 
					
						
							| 
									
										
										
										
											2023-05-23 23:55:21 +08:00
										 |  |  |           // Log.debug(log);
 | 
					
						
							| 
									
										
										
										
											2023-05-16 14:58:24 +08:00
										 |  |  |         }; | 
					
						
							| 
									
										
										
										
											2023-03-07 09:33:59 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-10-20 22:19:01 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-07-02 11:46:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   Future<void> applyRules() async { | 
					
						
							|  |  |  |     ensureAtLeastOneParagraphExists(); | 
					
						
							|  |  |  |     ensureLastNodeIsEditable(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   Future<void> ensureLastNodeIsEditable() async { | 
					
						
							|  |  |  |     final editorState = this.editorState; | 
					
						
							|  |  |  |     if (editorState == null) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     final document = editorState.document; | 
					
						
							|  |  |  |     final lastNode = document.root.children.lastOrNull; | 
					
						
							|  |  |  |     if (lastNode == null || lastNode.delta == null) { | 
					
						
							|  |  |  |       final transaction = editorState.transaction; | 
					
						
							|  |  |  |       transaction.insertNode([document.root.children.length], paragraphNode()); | 
					
						
							|  |  |  |       await editorState.apply(transaction); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   Future<void> ensureAtLeastOneParagraphExists() async { | 
					
						
							|  |  |  |     final editorState = this.editorState; | 
					
						
							|  |  |  |     if (editorState == null) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     final document = editorState.document; | 
					
						
							|  |  |  |     if (document.root.children.isEmpty) { | 
					
						
							|  |  |  |       final transaction = editorState.transaction; | 
					
						
							|  |  |  |       transaction.insertNode([0], paragraphNode()); | 
					
						
							| 
									
										
										
										
											2023-08-17 14:50:48 +08:00
										 |  |  |       transaction.afterSelection = Selection.collapsed( | 
					
						
							|  |  |  |         Position(path: [0], offset: 0), | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2023-07-02 11:46:45 +08:00
										 |  |  |       await editorState.apply(transaction); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @freezed | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  | class DocumentEvent with _$DocumentEvent { | 
					
						
							|  |  |  |   const factory DocumentEvent.initial() = Initial; | 
					
						
							| 
									
										
										
										
											2023-06-05 13:10:14 +08:00
										 |  |  |   const factory DocumentEvent.moveToTrash() = MoveToTrash; | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  |   const factory DocumentEvent.restore() = Restore; | 
					
						
							|  |  |  |   const factory DocumentEvent.restorePage() = RestorePage; | 
					
						
							|  |  |  |   const factory DocumentEvent.deletePermanently() = DeletePermanently; | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @freezed | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  | class DocumentState with _$DocumentState { | 
					
						
							|  |  |  |   const factory DocumentState({ | 
					
						
							|  |  |  |     required DocumentLoadingState loadingState, | 
					
						
							| 
									
										
										
										
											2021-10-31 17:24:55 +08:00
										 |  |  |     required bool isDeleted, | 
					
						
							| 
									
										
										
										
											2021-10-31 20:27:37 +08:00
										 |  |  |     required bool forceClose, | 
					
						
							| 
									
										
										
										
											2023-02-16 10:17:08 +08:00
										 |  |  |     UserProfilePB? userProfilePB, | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  |   }) = _DocumentState; | 
					
						
							| 
									
										
										
										
											2021-10-19 13:56:11 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  |   factory DocumentState.initial() => const DocumentState( | 
					
						
							|  |  |  |         loadingState: _Loading(), | 
					
						
							| 
									
										
										
										
											2021-10-31 17:24:55 +08:00
										 |  |  |         isDeleted: false, | 
					
						
							| 
									
										
										
										
											2021-10-31 20:27:37 +08:00
										 |  |  |         forceClose: false, | 
					
						
							| 
									
										
										
										
											2023-02-16 10:17:08 +08:00
										 |  |  |         userProfilePB: null, | 
					
						
							| 
									
										
										
										
											2021-10-31 17:24:55 +08:00
										 |  |  |       ); | 
					
						
							| 
									
										
										
										
											2021-10-19 13:56:11 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @freezed | 
					
						
							| 
									
										
										
										
											2022-02-23 22:17:47 +08:00
										 |  |  | class DocumentLoadingState with _$DocumentLoadingState { | 
					
						
							|  |  |  |   const factory DocumentLoadingState.loading() = _Loading; | 
					
						
							| 
									
										
										
										
											2022-08-09 10:35:27 +08:00
										 |  |  |   const factory DocumentLoadingState.finish( | 
					
						
							| 
									
										
										
										
											2023-05-23 16:13:12 +08:00
										 |  |  |     Either<FlowyError, DocumentDataPB> successOrFail, | 
					
						
							| 
									
										
										
										
											2023-04-10 15:10:42 +08:00
										 |  |  |   ) = _Finish; | 
					
						
							| 
									
										
										
										
											2021-07-24 18:55:13 +08:00
										 |  |  | } |