261 lines
7.9 KiB
Dart
Raw Normal View History

import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
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';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
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, Node;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/log.dart';
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';
2021-10-19 13:56:11 +08:00
import 'package:dartz/dartz.dart';
import 'dart:async';
import 'package:appflowy/util/either_extension.dart';
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;
EditorState? editorState;
StreamSubscription? _subscription;
2021-07-24 18:55:13 +08:00
2022-02-23 22:17:47 +08:00
DocumentBloc({
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 {
final result = await _trashService.deleteViews([view.id]);
2022-03-01 21:09:52 +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);
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 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 {
final userProfile = await UserBackendService.getCurrentUserProfile();
if (userProfile.isRight()) {
return emit(
state.copyWith(
loadingState: DocumentLoadingState.finish(
right(userProfile.asRight()),
),
),
);
}
2022-10-26 10:38:57 +08:00
final result = await _documentService.openDocument(view: view);
return result.fold(
(documentData) async {
await _initEditorState(documentData).whenComplete(() {
emit(
state.copyWith(
loadingState: DocumentLoadingState.finish(left(unit)),
userProfilePB: userProfile.asLeft(),
),
);
});
2022-10-22 21:57:44 +08:00
},
(err) async {
2022-10-22 21:57:44 +08:00
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) {},
);
},
);
}
Future<void> _initEditorState(DocumentDataPB documentData) async {
final document = Document.fromJson(jsonDecode(documentData.content));
final editorState = EditorState(document: document);
this.editorState = editorState;
// listen on document change
_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),
);
});
});
// log
if (kDebugMode) {
editorState.logConfiguration.handler = (log) {
Log.debug(log);
};
}
// migration
final migration = DocumentMigration(editorState: editorState);
await migration.apply();
}
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,
required bool isDeleted,
required bool forceClose,
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(),
isDeleted: false,
forceClose: false,
userProfilePB: null,
);
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;
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;
}
}
class DocumentMigration {
const DocumentMigration({
required this.editorState,
});
final EditorState editorState;
/// Migrate the document to the latest version.
Future<void> apply() async {
final transaction = editorState.transaction;
// A temporary solution to migrate the document to the latest version.
// Once the editor is stable, we can remove this.
// cover plugin
if (editorState.document.nodeAtPath([0])?.type != kCoverType) {
transaction.insertNode(
[0],
Node(type: kCoverType),
);
}
transaction.afterSelection = null;
if (transaction.operations.isNotEmpty) {
editorState.apply(transaction);
}
}
}