2021-10-20 22:19:01 +08:00
|
|
|
import 'dart:convert';
|
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';
|
|
|
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
|
|
|
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
2023-02-16 10:17:08 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
2022-10-22 21:57:44 +08:00
|
|
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
|
|
|
show EditorState, Document, Transaction;
|
2023-01-08 12:10:53 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
|
|
import 'package:appflowy_backend/log.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';
|
2021-10-19 13:56:11 +08:00
|
|
|
import 'package:dartz/dartz.dart';
|
2021-10-20 22:19:01 +08:00
|
|
|
import 'dart:async';
|
2023-02-26 16:27:17 +08:00
|
|
|
import 'package:appflowy/util/either_extension.dart';
|
2022-02-28 21:17:08 -05: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> {
|
2022-07-19 14:11:29 +08:00
|
|
|
final ViewPB view;
|
2022-10-26 10:38:57 +08:00
|
|
|
final DocumentService _documentService;
|
2022-03-01 21:09:52 +08:00
|
|
|
|
2022-10-26 10:38:57 +08:00
|
|
|
final ViewListener _listener;
|
|
|
|
final TrashService _trashService;
|
2023-02-24 09:16:51 +08:00
|
|
|
EditorState? editorState;
|
2021-11-04 12:47:41 +08:00
|
|
|
StreamSubscription? _subscription;
|
2021-07-24 18:55:13 +08:00
|
|
|
|
2022-02-23 22:17:47 +08:00
|
|
|
DocumentBloc({
|
2021-10-31 19:48:20 +08:00
|
|
|
required this.view,
|
2022-10-26 10:38:57 +08:00
|
|
|
}) : _documentService = DocumentService(),
|
|
|
|
_listener = ViewListener(view: view),
|
|
|
|
_trashService = TrashService(),
|
|
|
|
super(DocumentState.initial()) {
|
2022-02-23 22:17:47 +08:00
|
|
|
on<DocumentEvent>((event, emit) async {
|
2022-01-04 22:44:03 +08:00
|
|
|
await event.map(
|
|
|
|
initial: (Initial value) async {
|
|
|
|
await _initial(value, emit);
|
2022-10-22 21:57:44 +08:00
|
|
|
_listenOnViewChange();
|
2022-01-04 22:44:03 +08:00
|
|
|
},
|
|
|
|
deleted: (Deleted value) async {
|
|
|
|
emit(state.copyWith(isDeleted: true));
|
|
|
|
},
|
|
|
|
restore: (Restore value) async {
|
|
|
|
emit(state.copyWith(isDeleted: false));
|
|
|
|
},
|
|
|
|
deletePermanently: (DeletePermanently value) async {
|
2022-10-26 10:38:57 +08:00
|
|
|
final result = await _trashService
|
2022-08-09 10:35:27 +08:00
|
|
|
.deleteViews([Tuple2(view.id, TrashType.TrashView)]);
|
2022-03-01 21:09:52 +08:00
|
|
|
|
2022-08-09 10:35:27 +08:00
|
|
|
final newState = result.fold(
|
|
|
|
(l) => state.copyWith(forceClose: true), (r) => state);
|
2022-01-04 22:44:03 +08:00
|
|
|
emit(newState);
|
|
|
|
},
|
|
|
|
restorePage: (RestorePage value) async {
|
2022-10-26 10:38:57 +08:00
|
|
|
final result = await _trashService.putback(view.id);
|
2022-08-09 10:35:27 +08:00
|
|
|
final newState = result.fold(
|
|
|
|
(l) => state.copyWith(isDeleted: false), (r) => state);
|
2022-01-04 22:44:03 +08:00
|
|
|
emit(newState);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
2021-07-24 18:55:13 +08:00
|
|
|
}
|
2021-09-11 21:30:58 +08:00
|
|
|
|
2021-10-19 13:56:11 +08:00
|
|
|
@override
|
|
|
|
Future<void> close() async {
|
2022-10-26 10:38:57 +08:00
|
|
|
await _listener.stop();
|
2021-11-03 22:04:45 +08:00
|
|
|
|
2021-11-03 23:03:42 +08:00
|
|
|
if (_subscription != null) {
|
|
|
|
await _subscription?.cancel();
|
|
|
|
}
|
|
|
|
|
2022-10-26 10:38:57 +08:00
|
|
|
await _documentService.closeDocument(docId: view.id);
|
2021-10-19 13:56:11 +08:00
|
|
|
return super.close();
|
|
|
|
}
|
|
|
|
|
2022-02-23 22:17:47 +08:00
|
|
|
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
2023-02-26 16:27:17 +08:00
|
|
|
final userProfile = await UserBackendService.getCurrentUserProfile();
|
2023-02-16 10:17:08 +08:00
|
|
|
if (userProfile.isRight()) {
|
|
|
|
emit(
|
|
|
|
state.copyWith(
|
|
|
|
loadingState:
|
|
|
|
DocumentLoadingState.finish(right(userProfile.asRight())),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
2022-10-26 10:38:57 +08:00
|
|
|
final result = await _documentService.openDocument(view: view);
|
2022-10-22 21:57:44 +08:00
|
|
|
result.fold(
|
2023-02-10 22:24:34 +08:00
|
|
|
(documentData) {
|
|
|
|
final document = Document.fromJson(jsonDecode(documentData.content));
|
2022-10-22 21:57:44 +08:00
|
|
|
editorState = EditorState(document: document);
|
|
|
|
_listenOnDocumentChange();
|
|
|
|
emit(
|
|
|
|
state.copyWith(
|
|
|
|
loadingState: DocumentLoadingState.finish(left(unit)),
|
2023-02-16 10:17:08 +08:00
|
|
|
userProfilePB: userProfile.asLeft(),
|
2022-10-22 21:57:44 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
(err) {
|
|
|
|
emit(
|
|
|
|
state.copyWith(
|
|
|
|
loadingState: DocumentLoadingState.finish(right(err)),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _listenOnViewChange() {
|
2022-10-26 10:38:57 +08:00
|
|
|
_listener.start(
|
2022-05-05 21:15:01 +08:00
|
|
|
onViewDeleted: (result) {
|
|
|
|
result.fold(
|
|
|
|
(view) => add(const DocumentEvent.deleted()),
|
|
|
|
(error) {},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
onViewRestored: (result) {
|
|
|
|
result.fold(
|
|
|
|
(view) => add(const DocumentEvent.restore()),
|
|
|
|
(error) {},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2021-10-20 22:19:01 +08:00
|
|
|
}
|
|
|
|
|
2022-10-22 21:57:44 +08:00
|
|
|
void _listenOnDocumentChange() {
|
2023-02-24 09:16:51 +08:00
|
|
|
_subscription = editorState?.transactionStream.listen((transaction) {
|
2022-10-22 21:57:44 +08:00
|
|
|
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
2022-10-26 10:38:57 +08:00
|
|
|
_documentService
|
|
|
|
.applyEdit(docId: view.id, operations: json)
|
|
|
|
.then((result) {
|
2022-10-22 21:57:44 +08:00
|
|
|
result.fold(
|
|
|
|
(l) => null,
|
|
|
|
(err) => Log.error(err),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
2021-10-20 22:19:01 +08:00
|
|
|
}
|
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;
|
|
|
|
const factory DocumentEvent.deleted() = Deleted;
|
|
|
|
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(
|
|
|
|
Either<Unit, FlowyError> successOrFail) = _Finish;
|
2021-07-24 18:55:13 +08:00
|
|
|
}
|
2022-10-22 21:57:44 +08:00
|
|
|
|
|
|
|
/// Uses to erase the different between appflowy editor and the backend
|
|
|
|
class TransactionAdaptor {
|
|
|
|
final Transaction transaction;
|
|
|
|
TransactionAdaptor(this.transaction);
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() {
|
|
|
|
final json = <String, dynamic>{};
|
|
|
|
if (transaction.operations.isNotEmpty) {
|
|
|
|
// The backend uses [0,0] as the beginning path, but the editor uses [0].
|
|
|
|
// So it needs to extend the path by inserting `0` at the head for all
|
|
|
|
// operations before passing to the backend.
|
|
|
|
json['operations'] = transaction.operations
|
|
|
|
.map((e) => e.copyWith(path: [0, ...e.path]).toJson())
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
if (transaction.afterSelection != null) {
|
|
|
|
final selection = transaction.afterSelection!;
|
|
|
|
final start = selection.start;
|
|
|
|
final end = selection.end;
|
|
|
|
json['after_selection'] = selection
|
|
|
|
.copyWith(
|
|
|
|
start: start.copyWith(path: [0, ...start.path]),
|
|
|
|
end: end.copyWith(path: [0, ...end.path]),
|
|
|
|
)
|
|
|
|
.toJson();
|
|
|
|
}
|
|
|
|
if (transaction.beforeSelection != null) {
|
|
|
|
final selection = transaction.beforeSelection!;
|
|
|
|
final start = selection.start;
|
|
|
|
final end = selection.end;
|
|
|
|
json['before_selection'] = selection
|
|
|
|
.copyWith(
|
|
|
|
start: start.copyWith(path: [0, ...start.path]),
|
|
|
|
end: end.copyWith(path: [0, ...end.path]),
|
|
|
|
)
|
|
|
|
.toJson();
|
|
|
|
}
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
}
|