mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Integrate appflowy editor (#1040)
This commit is contained in:
parent
8dff9dc67c
commit
ad9a4b7d71
93
frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt
Normal file
93
frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf
Normal file
Binary file not shown.
Binary file not shown.
@ -24,7 +24,7 @@ class BoardPluginBuilder implements PluginBuilder {
|
|||||||
PluginType get pluginType => PluginType.board;
|
PluginType get pluginType => PluginType.board;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewDataTypePB get dataType => ViewDataTypePB.Database;
|
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;
|
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;
|
||||||
|
@ -2,10 +2,11 @@ import 'dart:convert';
|
|||||||
import 'package:app_flowy/plugins/trash/application/trash_service.dart';
|
import 'package:app_flowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:app_flowy/plugins/doc/application/doc_service.dart';
|
import 'package:app_flowy/plugins/doc/application/doc_service.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||||
|
show EditorState, Document, Transaction;
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flutter_quill/flutter_quill.dart' show Document, Delta;
|
|
||||||
import 'package:flowy_sdk/log.dart';
|
import 'package:flowy_sdk/log.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
@ -14,15 +15,13 @@ import 'dart:async';
|
|||||||
|
|
||||||
part 'doc_bloc.freezed.dart';
|
part 'doc_bloc.freezed.dart';
|
||||||
|
|
||||||
typedef FlutterQuillDocument = Document;
|
|
||||||
|
|
||||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final DocumentService service;
|
final DocumentService service;
|
||||||
|
|
||||||
final ViewListener listener;
|
final ViewListener listener;
|
||||||
final TrashService trashService;
|
final TrashService trashService;
|
||||||
late FlutterQuillDocument document;
|
late EditorState editorState;
|
||||||
StreamSubscription? _subscription;
|
StreamSubscription? _subscription;
|
||||||
|
|
||||||
DocumentBloc({
|
DocumentBloc({
|
||||||
@ -35,6 +34,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
await event.map(
|
await event.map(
|
||||||
initial: (Initial value) async {
|
initial: (Initial value) async {
|
||||||
await _initial(value, emit);
|
await _initial(value, emit);
|
||||||
|
_listenOnViewChange();
|
||||||
},
|
},
|
||||||
deleted: (Deleted value) async {
|
deleted: (Deleted value) async {
|
||||||
emit(state.copyWith(isDeleted: true));
|
emit(state.copyWith(isDeleted: true));
|
||||||
@ -73,6 +73,29 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
||||||
|
final result = await service.openDocument(view: view);
|
||||||
|
result.fold(
|
||||||
|
(block) {
|
||||||
|
final document = Document.fromJson(jsonDecode(block.snapshot));
|
||||||
|
editorState = EditorState(document: document);
|
||||||
|
_listenOnDocumentChange();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
loadingState: DocumentLoadingState.finish(left(unit)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(err) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
loadingState: DocumentLoadingState.finish(right(err)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenOnViewChange() {
|
||||||
listener.start(
|
listener.start(
|
||||||
onViewDeleted: (result) {
|
onViewDeleted: (result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
@ -87,46 +110,18 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final result = await service.openDocument(docId: view.id);
|
}
|
||||||
|
|
||||||
|
void _listenOnDocumentChange() {
|
||||||
|
_subscription = editorState.transactionStream.listen((transaction) {
|
||||||
|
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
||||||
|
service.applyEdit(docId: view.id, operations: json).then((result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(block) {
|
(l) => null,
|
||||||
document = _decodeJsonToDocument(block.snapshot);
|
(err) => Log.error(err),
|
||||||
_subscription = document.changes.listen((event) {
|
);
|
||||||
final delta = event.item2;
|
});
|
||||||
final documentDelta = document.toDelta();
|
|
||||||
_composeDelta(delta, documentDelta);
|
|
||||||
});
|
});
|
||||||
emit(state.copyWith(
|
|
||||||
loadingState: DocumentLoadingState.finish(left(unit))));
|
|
||||||
},
|
|
||||||
(err) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
loadingState: DocumentLoadingState.finish(right(err))));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Document _decodeListToDocument(Uint8List data) {
|
|
||||||
// final json = jsonDecode(utf8.decode(data));
|
|
||||||
// final document = Document.fromJson(json);
|
|
||||||
// return document;
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _composeDelta(Delta composedDelta, Delta documentDelta) async {
|
|
||||||
final json = jsonEncode(composedDelta.toJson());
|
|
||||||
Log.debug("doc_id: $view.id - Send json: $json");
|
|
||||||
final result = await service.applyEdit(docId: view.id, operations: json);
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
(_) {},
|
|
||||||
(r) => Log.error(r),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Document _decodeJsonToDocument(String data) {
|
|
||||||
final json = jsonDecode(data);
|
|
||||||
final document = Document.fromJson(json);
|
|
||||||
return document;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,3 +155,44 @@ class DocumentLoadingState with _$DocumentLoadingState {
|
|||||||
const factory DocumentLoadingState.finish(
|
const factory DocumentLoadingState.finish(
|
||||||
Either<Unit, FlowyError> successOrFail) = _Finish;
|
Either<Unit, FlowyError> successOrFail) = _Finish;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,16 +3,25 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
|||||||
|
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-sync/document.pb.dart';
|
|
||||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||||
|
|
||||||
class DocumentService {
|
class DocumentService {
|
||||||
Future<Either<DocumentSnapshotPB, FlowyError>> openDocument({
|
Future<Either<DocumentSnapshotPB, FlowyError>> openDocument({
|
||||||
required String docId,
|
required ViewPB view,
|
||||||
}) async {
|
}) async {
|
||||||
await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
|
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||||
|
|
||||||
|
final payload = OpenDocumentContextPB()
|
||||||
|
..documentId = view.id
|
||||||
|
..documentVersion = DocumentVersionPB.V1;
|
||||||
|
// switch (view.dataFormat) {
|
||||||
|
// case ViewDataFormatPB.DeltaFormat:
|
||||||
|
// payload.documentVersion = DocumentVersionPB.V0;
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
final payload = DocumentIdPB(value: docId);
|
|
||||||
return DocumentEventGetDocument(payload).send();
|
return DocumentEventGetDocument(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
|
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
|
||||||
import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
|
|
||||||
import 'package:app_flowy/plugins/doc/application/share_service.dart';
|
import 'package:app_flowy/plugins/doc/application/share_service.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/document_markdown.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
|
||||||
part 'share_bloc.freezed.dart';
|
part 'share_bloc.freezed.dart';
|
||||||
|
|
||||||
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||||
@ -19,10 +21,10 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
|||||||
on<DocShareEvent>((event, emit) async {
|
on<DocShareEvent>((event, emit) async {
|
||||||
await event.map(
|
await event.map(
|
||||||
shareMarkdown: (ShareMarkdown value) async {
|
shareMarkdown: (ShareMarkdown value) async {
|
||||||
await service.exportMarkdown(view.id).then((result) {
|
await service.exportMarkdown(view).then((result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(value) => emit(
|
(value) => emit(DocShareState.finish(
|
||||||
DocShareState.finish(left(_convertDeltaToMarkdown(value)))),
|
left(_convertDocumentToMarkdown(value)))),
|
||||||
(error) => emit(DocShareState.finish(right(error))),
|
(error) => emit(DocShareState.finish(right(error))),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -35,8 +37,10 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) {
|
ExportDataPB _convertDocumentToMarkdown(ExportDataPB value) {
|
||||||
final result = deltaToMarkdown(value.data);
|
final json = jsonDecode(value.data);
|
||||||
|
final document = Document.fromJson(json);
|
||||||
|
final result = documentToMarkdown(document);
|
||||||
value.data = result;
|
value.data = result;
|
||||||
writeFile(result);
|
writeFile(result);
|
||||||
return value;
|
return value;
|
||||||
|
@ -3,26 +3,28 @@ import 'package:dartz/dartz.dart';
|
|||||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
||||||
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
|
||||||
class ShareService {
|
class ShareService {
|
||||||
Future<Either<ExportDataPB, FlowyError>> export(
|
Future<Either<ExportDataPB, FlowyError>> export(
|
||||||
String docId, ExportType type) {
|
ViewPB view, ExportType type) {
|
||||||
final request = ExportPayloadPB.create()
|
var payload = ExportPayloadPB.create()
|
||||||
..viewId = docId
|
..viewId = view.id
|
||||||
..exportType = type;
|
..exportType = type
|
||||||
|
..documentVersion = DocumentVersionPB.V1;
|
||||||
|
|
||||||
return DocumentEventExportDocument(request).send();
|
return DocumentEventExportDocument(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<ExportDataPB, FlowyError>> exportText(String docId) {
|
Future<Either<ExportDataPB, FlowyError>> exportText(ViewPB view) {
|
||||||
return export(docId, ExportType.Text);
|
return export(view, ExportType.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<ExportDataPB, FlowyError>> exportMarkdown(String docId) {
|
Future<Either<ExportDataPB, FlowyError>> exportMarkdown(ViewPB view) {
|
||||||
return export(docId, ExportType.Markdown);
|
return export(view, ExportType.Markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<ExportDataPB, FlowyError>> exportURL(String docId) {
|
Future<Either<ExportDataPB, FlowyError>> exportURL(ViewPB view) {
|
||||||
return export(docId, ExportType.Link);
|
return export(view, ExportType.Link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class DocumentPluginBuilder extends PluginBuilder {
|
|||||||
PluginType get pluginType => PluginType.editor;
|
PluginType get pluginType => PluginType.editor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewDataTypePB get dataType => ViewDataTypePB.Text;
|
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentPlugin extends Plugin<int> {
|
class DocumentPlugin extends Plugin<int> {
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
|
import 'package:app_flowy/plugins/doc/editor_styles.dart';
|
||||||
|
import 'package:app_flowy/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart';
|
||||||
import 'package:app_flowy/startup/startup.dart';
|
import 'package:app_flowy/startup/startup.dart';
|
||||||
import 'package:app_flowy/workspace/application/appearance.dart';
|
|
||||||
import 'package:app_flowy/plugins/doc/presentation/banner.dart';
|
import 'package:app_flowy/plugins/doc/presentation/banner.dart';
|
||||||
import 'package:app_flowy/plugins/doc/presentation/toolbar/tool_bar.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'application/doc_bloc.dart';
|
import 'application/doc_bloc.dart';
|
||||||
import 'styles.dart';
|
|
||||||
|
|
||||||
class DocumentPage extends StatefulWidget {
|
class DocumentPage extends StatefulWidget {
|
||||||
final VoidCallback onDeleted;
|
final VoidCallback onDeleted;
|
||||||
@ -29,11 +26,12 @@ class DocumentPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _DocumentPageState extends State<DocumentPage> {
|
class _DocumentPageState extends State<DocumentPage> {
|
||||||
late DocumentBloc documentBloc;
|
late DocumentBloc documentBloc;
|
||||||
final scrollController = ScrollController();
|
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
// The appflowy editor use Intl as locatization, set the default language as fallback.
|
||||||
|
Intl.defaultLocale = 'en_US';
|
||||||
documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
|
documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
|
||||||
..add(const DocumentEvent.initial());
|
..add(const DocumentEvent.initial());
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -48,9 +46,9 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
child:
|
child:
|
||||||
BlocBuilder<DocumentBloc, DocumentState>(builder: (context, state) {
|
BlocBuilder<DocumentBloc, DocumentState>(builder: (context, state) {
|
||||||
return state.loadingState.map(
|
return state.loadingState.map(
|
||||||
// loading: (_) => const FlowyProgressIndicator(),
|
loading: (_) => SizedBox.expand(
|
||||||
loading: (_) =>
|
child: Container(color: Colors.transparent),
|
||||||
SizedBox.expand(child: Container(color: Colors.transparent)),
|
),
|
||||||
finish: (result) => result.successOrFail.fold(
|
finish: (result) => result.successOrFail.fold(
|
||||||
(_) {
|
(_) {
|
||||||
if (state.forceClose) {
|
if (state.forceClose) {
|
||||||
@ -75,24 +73,11 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _renderDocument(BuildContext context, DocumentState state) {
|
Widget _renderDocument(BuildContext context, DocumentState state) {
|
||||||
quill.QuillController controller = quill.QuillController(
|
|
||||||
document: context.read<DocumentBloc>().document,
|
|
||||||
selection: const TextSelection.collapsed(offset: 0),
|
|
||||||
);
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (state.isDeleted) _renderBanner(context),
|
if (state.isDeleted) _renderBanner(context),
|
||||||
Expanded(
|
// AppFlowy Editor
|
||||||
child: Column(
|
_renderAppFlowyEditor(context.read<DocumentBloc>().editorState),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
_renderEditor(controller),
|
|
||||||
const VSpace(10),
|
|
||||||
_renderToolbar(controller),
|
|
||||||
const VSpace(10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -107,36 +92,20 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _renderEditor(quill.QuillController controller) {
|
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||||
final editor = quill.QuillEditor(
|
final editor = AppFlowyEditor(
|
||||||
controller: controller,
|
editorState: editorState,
|
||||||
focusNode: _focusNode,
|
editorStyle: customEditorStyle(context),
|
||||||
scrollable: true,
|
customBuilders: {
|
||||||
paintCursorAboveText: true,
|
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||||
autoFocus: controller.document.isEmpty(),
|
},
|
||||||
expands: false,
|
shortcutEvents: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
insertHorizontalRule,
|
||||||
readOnly: false,
|
],
|
||||||
scrollBottomInset: 0,
|
|
||||||
scrollController: scrollController,
|
|
||||||
customStyles: customStyles(context),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: ScrollbarListStack(
|
child: SizedBox.expand(
|
||||||
axis: Axis.vertical,
|
child: editor,
|
||||||
controller: scrollController,
|
|
||||||
barSize: 6.0,
|
|
||||||
child: SizedBox.expand(child: editor),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _renderToolbar(quill.QuillController controller) {
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: Provider.of<AppearanceSetting>(context, listen: true),
|
|
||||||
child: EditorToolbar.basic(
|
|
||||||
controller: controller,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
61
frontend/app_flowy/lib/plugins/doc/editor_styles.dart
Normal file
61
frontend/app_flowy/lib/plugins/doc/editor_styles.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra/theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
EditorStyle customEditorStyle(BuildContext context) {
|
||||||
|
final theme = context.watch<AppTheme>();
|
||||||
|
const baseFontSize = 14.0;
|
||||||
|
const basePadding = 12.0;
|
||||||
|
var textStyle = theme.isDark
|
||||||
|
? BuiltInTextStyle.builtInDarkMode()
|
||||||
|
: BuiltInTextStyle.builtIn();
|
||||||
|
textStyle = textStyle.copyWith(
|
||||||
|
defaultTextStyle: textStyle.defaultTextStyle.copyWith(
|
||||||
|
fontFamily: 'poppins',
|
||||||
|
fontSize: baseFontSize,
|
||||||
|
),
|
||||||
|
bold: textStyle.bold.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return EditorStyle.defaultStyle().copyWith(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||||
|
textStyle: textStyle,
|
||||||
|
pluginStyles: {
|
||||||
|
'text/heading': builtInPluginStyle
|
||||||
|
..update(
|
||||||
|
'textStyle',
|
||||||
|
(_) => (EditorState editorState, Node node) {
|
||||||
|
final headingToFontSize = {
|
||||||
|
'h1': baseFontSize + 12,
|
||||||
|
'h2': baseFontSize + 8,
|
||||||
|
'h3': baseFontSize + 4,
|
||||||
|
'h4': baseFontSize,
|
||||||
|
'h5': baseFontSize,
|
||||||
|
'h6': baseFontSize,
|
||||||
|
};
|
||||||
|
final fontSize =
|
||||||
|
headingToFontSize[node.attributes.heading] ?? baseFontSize;
|
||||||
|
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..update(
|
||||||
|
'padding',
|
||||||
|
(_) => (EditorState editorState, Node node) {
|
||||||
|
final headingToPadding = {
|
||||||
|
'h1': basePadding + 6,
|
||||||
|
'h2': basePadding + 4,
|
||||||
|
'h3': basePadding + 2,
|
||||||
|
'h4': basePadding,
|
||||||
|
'h5': basePadding,
|
||||||
|
'h6': basePadding,
|
||||||
|
};
|
||||||
|
final padding =
|
||||||
|
headingToPadding[node.attributes.heading] ?? basePadding;
|
||||||
|
return EdgeInsets.only(bottom: padding);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ShortcutEvent insertHorizontalRule = ShortcutEvent(
|
||||||
|
key: 'Horizontal rule',
|
||||||
|
command: 'Minus',
|
||||||
|
handler: _insertHorzaontalRule,
|
||||||
|
);
|
||||||
|
|
||||||
|
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
if (textNodes.length != 1 || selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
if (textNode.toPlainText() == '--') {
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..deleteText(textNode, 0, 2)
|
||||||
|
..insertNode(
|
||||||
|
textNode.path,
|
||||||
|
Node(
|
||||||
|
type: 'horizontal_rule',
|
||||||
|
children: LinkedList(),
|
||||||
|
attributes: {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..afterSelection =
|
||||||
|
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||||
|
editorState.apply(transaction);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||||
|
name: () => 'Horizontal rule',
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.horizontal_rule,
|
||||||
|
color: Colors.black,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
keywords: ['horizontal rule'],
|
||||||
|
handler: (editorState, _, __) {
|
||||||
|
final selection =
|
||||||
|
editorState.service.selectionService.currentSelection.value;
|
||||||
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
if (selection == null || textNodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
if (textNode.toPlainText().isEmpty) {
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..insertNode(
|
||||||
|
textNode.path,
|
||||||
|
Node(
|
||||||
|
type: 'horizontal_rule',
|
||||||
|
children: LinkedList(),
|
||||||
|
attributes: {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..afterSelection =
|
||||||
|
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||||
|
editorState.apply(transaction);
|
||||||
|
} else {
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..insertNode(
|
||||||
|
selection.end.path.next,
|
||||||
|
TextNode(
|
||||||
|
children: LinkedList(),
|
||||||
|
attributes: {
|
||||||
|
'subtype': 'horizontal_rule',
|
||||||
|
},
|
||||||
|
delta: Delta()..insert('---'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..afterSelection = selection;
|
||||||
|
editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
|
@override
|
||||||
|
Widget build(NodeWidgetContext<Node> context) {
|
||||||
|
return _HorizontalRuleWidget(
|
||||||
|
key: context.node.key,
|
||||||
|
node: context.node,
|
||||||
|
editorState: context.editorState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeValidator<Node> get nodeValidator => (node) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorizontalRuleWidget extends StatefulWidget {
|
||||||
|
const _HorizontalRuleWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.node,
|
||||||
|
required this.editorState,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
|
||||||
|
with SelectableMixin {
|
||||||
|
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position start() => Position(path: widget.node.path, offset: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position end() => Position(path: widget.node.path, offset: 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position getPositionInOffset(Offset start) => end();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get shouldCursorBlink => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CursorStyle get cursorStyle => CursorStyle.borderLine;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect? getCursorRectInPosition(Position position) {
|
||||||
|
final size = _renderBox.size;
|
||||||
|
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Rect> getRectsInSelection(Selection selection) =>
|
||||||
|
[Offset.zero & _renderBox.size];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
|
||||||
|
path: widget.node.path,
|
||||||
|
startOffset: 0,
|
||||||
|
endOffset: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||||
|
}
|
@ -26,7 +26,7 @@ class GridPluginBuilder implements PluginBuilder {
|
|||||||
PluginType get pluginType => PluginType.grid;
|
PluginType get pluginType => PluginType.grid;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewDataTypePB get dataType => ViewDataTypePB.Database;
|
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid;
|
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid;
|
||||||
|
@ -49,7 +49,7 @@ abstract class PluginBuilder {
|
|||||||
|
|
||||||
PluginType get pluginType;
|
PluginType get pluginType;
|
||||||
|
|
||||||
ViewDataTypePB get dataType => ViewDataTypePB.Text;
|
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat;
|
||||||
|
|
||||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document;
|
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import 'package:app_flowy/startup/startup.dart';
|
import 'package:app_flowy/startup/startup.dart';
|
||||||
import 'package:app_flowy/user/application/user_settings_service.dart';
|
import 'package:app_flowy/user/application/user_settings_service.dart';
|
||||||
import 'package:app_flowy/workspace/application/appearance.dart';
|
import 'package:app_flowy/workspace/application/appearance.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_sdk/log.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:window_size/window_size.dart';
|
import 'package:window_size/window_size.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:flowy_sdk/log.dart';
|
|
||||||
|
|
||||||
class InitAppWidgetTask extends LaunchTask {
|
class InitAppWidgetTask extends LaunchTask {
|
||||||
@override
|
@override
|
||||||
@ -93,7 +94,8 @@ class ApplicationWidget extends StatelessWidget {
|
|||||||
builder: overlayManagerBuilder(),
|
builder: overlayManagerBuilder(),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: theme.themeData,
|
theme: theme.themeData,
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates +
|
||||||
|
[AppFlowyEditorLocalizations.delegate],
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
navigatorKey: AppGlobals.rootNavKey,
|
navigatorKey: AppGlobals.rootNavKey,
|
||||||
|
@ -98,7 +98,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
|
|||||||
appId: app.id,
|
appId: app.id,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
desc: value.desc ?? "",
|
desc: value.desc ?? "",
|
||||||
dataType: value.pluginBuilder.dataType,
|
dataFormatType: value.pluginBuilder.dataFormatType,
|
||||||
pluginType: value.pluginBuilder.pluginType,
|
pluginType: value.pluginBuilder.pluginType,
|
||||||
layoutType: value.pluginBuilder.layoutType!,
|
layoutType: value.pluginBuilder.layoutType!,
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ class AppService {
|
|||||||
required String appId,
|
required String appId,
|
||||||
required String name,
|
required String name,
|
||||||
String? desc,
|
String? desc,
|
||||||
required ViewDataTypePB dataType,
|
required ViewDataFormatPB dataFormatType,
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
required ViewLayoutTypePB layoutType,
|
required ViewLayoutTypePB layoutType,
|
||||||
}) {
|
}) {
|
||||||
@ -27,7 +27,7 @@ class AppService {
|
|||||||
..belongToId = appId
|
..belongToId = appId
|
||||||
..name = name
|
..name = name
|
||||||
..desc = desc ?? ""
|
..desc = desc ?? ""
|
||||||
..dataType = dataType
|
..dataFormat = dataFormatType
|
||||||
..layout = layoutType;
|
..layout = layoutType;
|
||||||
|
|
||||||
return FolderEventCreateView(payload).send();
|
return FolderEventCreateView(payload).send();
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
library delta_markdown;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/markdown_encoder.dart';
|
||||||
|
|
||||||
|
/// Codec used to convert between Markdown and AppFlowy Editor Document.
|
||||||
|
const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec();
|
||||||
|
|
||||||
|
Document markdownToDocument(String markdown) {
|
||||||
|
return _kCodec.decode(markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
String documentToMarkdown(Document document) {
|
||||||
|
return _kCodec.encode(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
|
||||||
|
const AppFlowyEditorMarkdownCodec();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Converter<String, Document> get decoder => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Converter<Document, String> get encoder {
|
||||||
|
return AppFlowyEditorMarkdownEncoder();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
class ImageNodeParser extends NodeParser {
|
||||||
|
const ImageNodeParser();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => 'image';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String transform(Node node) {
|
||||||
|
return '![](${node.attributes['image_src']})';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/image_node_parser.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/text_node_parser.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
class AppFlowyEditorMarkdownEncoder extends Converter<Document, String> {
|
||||||
|
AppFlowyEditorMarkdownEncoder({
|
||||||
|
this.parsers = const [
|
||||||
|
TextNodeParser(),
|
||||||
|
ImageNodeParser(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<NodeParser> parsers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String convert(Document input) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final node in input.root.children) {
|
||||||
|
NodeParser? parser =
|
||||||
|
parsers.firstWhereOrNull((element) => element.id == node.type);
|
||||||
|
if (parser != null) {
|
||||||
|
buffer.write(parser.transform(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IterableExtension<T> on Iterable<T> {
|
||||||
|
T? firstWhereOrNull(bool Function(T element) test) {
|
||||||
|
for (var element in this) {
|
||||||
|
if (test(element)) return element;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
abstract class NodeParser {
|
||||||
|
const NodeParser();
|
||||||
|
|
||||||
|
String get id;
|
||||||
|
String transform(Node node);
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
|
||||||
|
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
class TextNodeParser extends NodeParser {
|
||||||
|
const TextNodeParser();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => 'text';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String transform(Node node) {
|
||||||
|
assert(node is TextNode);
|
||||||
|
final textNode = node as TextNode;
|
||||||
|
final delta = jsonEncode(
|
||||||
|
textNode.delta
|
||||||
|
..add(TextInsert('\n'))
|
||||||
|
..toJson(),
|
||||||
|
);
|
||||||
|
final markdown = deltaToMarkdown(delta);
|
||||||
|
final attributes = textNode.attributes;
|
||||||
|
var result = markdown;
|
||||||
|
var suffix = '';
|
||||||
|
if (attributes.isNotEmpty &&
|
||||||
|
attributes.containsKey(BuiltInAttributeKey.subtype)) {
|
||||||
|
final subtype = attributes[BuiltInAttributeKey.subtype];
|
||||||
|
if (node.next?.subtype != subtype) {
|
||||||
|
suffix = '\n';
|
||||||
|
}
|
||||||
|
if (subtype == 'heading') {
|
||||||
|
final heading = attributes[BuiltInAttributeKey.heading];
|
||||||
|
if (heading == 'h1') {
|
||||||
|
result = '# $markdown';
|
||||||
|
} else if (heading == 'h2') {
|
||||||
|
result = '## $markdown';
|
||||||
|
} else if (heading == 'h3') {
|
||||||
|
result = '### $markdown';
|
||||||
|
} else if (heading == 'h4') {
|
||||||
|
result = '#### $markdown';
|
||||||
|
} else if (heading == 'h5') {
|
||||||
|
result = '##### $markdown';
|
||||||
|
} else if (heading == 'h6') {
|
||||||
|
result = '###### $markdown';
|
||||||
|
}
|
||||||
|
} else if (subtype == 'quote') {
|
||||||
|
result = '> $markdown';
|
||||||
|
} else if (subtype == 'code') {
|
||||||
|
result = '`$markdown`';
|
||||||
|
} else if (subtype == 'code-block') {
|
||||||
|
result = '```\n$markdown\n```';
|
||||||
|
} else if (subtype == 'bulleted-list') {
|
||||||
|
result = '- $markdown';
|
||||||
|
} else if (subtype == 'number-list') {
|
||||||
|
final number = attributes['number'];
|
||||||
|
result = '$number. $markdown';
|
||||||
|
} else if (subtype == 'checkbox') {
|
||||||
|
if (attributes[BuiltInAttributeKey.checkbox] == true) {
|
||||||
|
result = '- [x] $markdown';
|
||||||
|
} else {
|
||||||
|
result = '- [ ] $markdown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '$result$suffix';
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
duplicate: (e) async {
|
duplicate: (e) async {
|
||||||
final result = await service.duplicate(viewId: view.id);
|
final result = await service.duplicate(view: view);
|
||||||
emit(
|
emit(
|
||||||
result.fold(
|
result.fold(
|
||||||
(l) => state.copyWith(successOrFailure: left(unit)),
|
(l) => state.copyWith(successOrFailure: left(unit)),
|
||||||
|
@ -10,7 +10,8 @@ class ViewService {
|
|||||||
return FolderEventReadView(request).send();
|
return FolderEventReadView(request).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<ViewPB, FlowyError>> updateView({required String viewId, String? name, String? desc}) {
|
Future<Either<ViewPB, FlowyError>> updateView(
|
||||||
|
{required String viewId, String? name, String? desc}) {
|
||||||
final request = UpdateViewPayloadPB.create()..viewId = viewId;
|
final request = UpdateViewPayloadPB.create()..viewId = viewId;
|
||||||
|
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
@ -29,8 +30,7 @@ class ViewService {
|
|||||||
return FolderEventDeleteView(request).send();
|
return FolderEventDeleteView(request).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> duplicate({required String viewId}) {
|
Future<Either<Unit, FlowyError>> duplicate({required ViewPB view}) {
|
||||||
final request = ViewIdPB(value: viewId);
|
return FolderEventDuplicateView(view).send();
|
||||||
return FolderEventDuplicateView(request).send();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,12 @@
|
|||||||
"document": {
|
"document": {
|
||||||
"type": "editor",
|
"type": "editor",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "image",
|
|
||||||
"attributes": {
|
|
||||||
"image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg",
|
|
||||||
"align": "center"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"attributes": { "subtype": "heading", "heading": "h1" },
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
"delta": [
|
"delta": [
|
||||||
{ "insert": "👋 " },
|
{ "insert": "👋 " },
|
||||||
{ "insert": "Welcome to ", "attributes": { "bold": true } },
|
{ "insert": "Welcome to ", "attributes": { "bold": true } },
|
||||||
|
@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:example/plugin/underscore_to_italic.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@ -141,7 +140,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
enterInCodeBlock,
|
enterInCodeBlock,
|
||||||
ignoreKeysInCodeBlock,
|
ignoreKeysInCodeBlock,
|
||||||
underscoreToItalic,
|
|
||||||
insertHorizontalRule,
|
insertHorizontalRule,
|
||||||
],
|
],
|
||||||
selectionMenuItems: [
|
selectionMenuItems: [
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
ShortcutEvent underscoreToItalic = ShortcutEvent(
|
|
||||||
key: 'Underscore to italic',
|
|
||||||
command: 'shift+underscore',
|
|
||||||
handler: _underscoreToItalicHandler,
|
|
||||||
);
|
|
||||||
|
|
||||||
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
|
||||||
// Obtain the selection and selected nodes of the current document through the 'selectionService'
|
|
||||||
// to determine whether the selection is collapsed and whether the selected node is a text node.
|
|
||||||
final selectionService = editorState.service.selectionService;
|
|
||||||
final selection = selectionService.currentSelection.value;
|
|
||||||
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
|
|
||||||
if (selection == null || !selection.isSingle || textNodes.length != 1) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final textNode = textNodes.first;
|
|
||||||
final text = textNode.toPlainText();
|
|
||||||
// Determine if an 'underscore' already exists in the text node and only once.
|
|
||||||
final firstUnderscore = text.indexOf('_');
|
|
||||||
final lastUnderscore = text.lastIndexOf('_');
|
|
||||||
if (firstUnderscore == -1 ||
|
|
||||||
firstUnderscore != lastUnderscore ||
|
|
||||||
firstUnderscore == selection.start.offset - 1) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the previous 'underscore',
|
|
||||||
// update the style of the text surrounded by the two underscores to 'italic',
|
|
||||||
// and update the cursor position.
|
|
||||||
final transaction = editorState.transaction
|
|
||||||
..deleteText(textNode, firstUnderscore, 1)
|
|
||||||
..formatText(
|
|
||||||
textNode,
|
|
||||||
firstUnderscore,
|
|
||||||
selection.end.offset - firstUnderscore - 1,
|
|
||||||
{
|
|
||||||
BuiltInAttributeKey.italic: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
..afterSelection = Selection.collapsed(
|
|
||||||
Position(
|
|
||||||
path: textNode.path,
|
|
||||||
offset: selection.end.offset - 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
editorState.apply(transaction);
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
};
|
|
@ -57,8 +57,8 @@ class Document {
|
|||||||
|
|
||||||
final parent = nodeAtPath(path.parent);
|
final parent = nodeAtPath(path.parent);
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
for (final node in nodes) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
parent.insert(node, index: path.last);
|
parent.insert(nodes.elementAt(i), index: path.last + i);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ class Transaction {
|
|||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
if (operations.isNotEmpty) {
|
if (operations.isNotEmpty) {
|
||||||
json['operations'] = operations.map((o) => o.toJson());
|
json['operations'] = operations.map((o) => o.toJson()).toList();
|
||||||
}
|
}
|
||||||
if (afterSelection != null) {
|
if (afterSelection != null) {
|
||||||
json['after_selection'] = afterSelection!.toJson();
|
json['after_selection'] = afterSelection!.toJson();
|
||||||
|
@ -0,0 +1,904 @@
|
|||||||
|
// TODO: Remove this file until we update the flutter version to 3.5.x
|
||||||
|
//
|
||||||
|
// This file is copied from flutter(3.5.x) repo.
|
||||||
|
//
|
||||||
|
// We Need to commit(https://github.com/flutter/flutter/pull/113770) to fix the
|
||||||
|
// overflow issue.
|
||||||
|
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// A place in an [Overlay] that can contain a widget.
|
||||||
|
///
|
||||||
|
/// Overlay entries are inserted into an [Overlay] using the
|
||||||
|
/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the
|
||||||
|
/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of]
|
||||||
|
/// function.
|
||||||
|
///
|
||||||
|
/// An overlay entry can be in at most one overlay at a time. To remove an entry
|
||||||
|
/// from its overlay, call the [remove] function on the overlay entry.
|
||||||
|
///
|
||||||
|
/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
|
||||||
|
/// [Positioned] and [AnimatedPositioned] to position themselves within the
|
||||||
|
/// overlay.
|
||||||
|
///
|
||||||
|
/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
|
||||||
|
/// follows the user's finger across the screen after the drag begins. Using the
|
||||||
|
/// overlay to display the drag avatar lets the avatar float over the other
|
||||||
|
/// widgets in the app. As the user's finger moves, draggable calls
|
||||||
|
/// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build,
|
||||||
|
/// the entry includes a [Positioned] with its top and left property set to
|
||||||
|
/// position the drag avatar near the user's finger. When the drag is over,
|
||||||
|
/// [Draggable] removes the entry from the overlay to remove the drag avatar
|
||||||
|
/// from view.
|
||||||
|
///
|
||||||
|
/// By default, if there is an entirely [opaque] entry over this one, then this
|
||||||
|
/// one will not be included in the widget tree (in particular, stateful widgets
|
||||||
|
/// within the overlay entry will not be instantiated). To ensure that your
|
||||||
|
/// overlay entry is still built even if it is not visible, set [maintainState]
|
||||||
|
/// to true. This is more expensive, so should be done with care. In particular,
|
||||||
|
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
|
||||||
|
/// call [State.setState], the user's battery will be drained unnecessarily.
|
||||||
|
///
|
||||||
|
/// [OverlayEntry] is a [ChangeNotifier] that notifies when the widget built by
|
||||||
|
/// [builder] is mounted or unmounted, whose exact state can be queried by
|
||||||
|
/// [mounted].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Overlay]
|
||||||
|
/// * [OverlayState]
|
||||||
|
/// * [WidgetsApp]
|
||||||
|
/// * [MaterialApp]
|
||||||
|
class OverlayEntry extends ChangeNotifier {
|
||||||
|
/// Creates an overlay entry.
|
||||||
|
///
|
||||||
|
/// To insert the entry into an [Overlay], first find the overlay using
|
||||||
|
/// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
|
||||||
|
/// call [remove] on the overlay entry itself.
|
||||||
|
OverlayEntry({
|
||||||
|
required this.builder,
|
||||||
|
bool opaque = false,
|
||||||
|
bool maintainState = false,
|
||||||
|
}) : _opaque = opaque,
|
||||||
|
_maintainState = maintainState;
|
||||||
|
|
||||||
|
/// This entry will include the widget built by this builder in the overlay at
|
||||||
|
/// the entry's position.
|
||||||
|
///
|
||||||
|
/// To cause this builder to be called again, call [markNeedsBuild] on this
|
||||||
|
/// overlay entry.
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
/// Whether this entry occludes the entire overlay.
|
||||||
|
///
|
||||||
|
/// If an entry claims to be opaque, then, for efficiency, the overlay will
|
||||||
|
/// skip building entries below that entry unless they have [maintainState]
|
||||||
|
/// set.
|
||||||
|
bool get opaque => _opaque;
|
||||||
|
bool _opaque;
|
||||||
|
set opaque(bool value) {
|
||||||
|
if (_opaque == value) return;
|
||||||
|
_opaque = value;
|
||||||
|
_overlay?._didChangeEntryOpacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this entry must be included in the tree even if there is a fully
|
||||||
|
/// [opaque] entry above it.
|
||||||
|
///
|
||||||
|
/// By default, if there is an entirely [opaque] entry over this one, then this
|
||||||
|
/// one will not be included in the widget tree (in particular, stateful widgets
|
||||||
|
/// within the overlay entry will not be instantiated). To ensure that your
|
||||||
|
/// overlay entry is still built even if it is not visible, set [maintainState]
|
||||||
|
/// to true. This is more expensive, so should be done with care. In particular,
|
||||||
|
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
|
||||||
|
/// call [State.setState], the user's battery will be drained unnecessarily.
|
||||||
|
///
|
||||||
|
/// This is used by the [Navigator] and [Route] objects to ensure that routes
|
||||||
|
/// are kept around even when in the background, so that [Future]s promised
|
||||||
|
/// from subsequent routes will be handled properly when they complete.
|
||||||
|
bool get maintainState => _maintainState;
|
||||||
|
bool _maintainState;
|
||||||
|
set maintainState(bool value) {
|
||||||
|
if (_maintainState == value) return;
|
||||||
|
_maintainState = value;
|
||||||
|
assert(_overlay != null);
|
||||||
|
_overlay!._didChangeEntryOpacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the [OverlayEntry] is currently mounted in the widget tree.
|
||||||
|
///
|
||||||
|
/// The [OverlayEntry] notifies its listeners when this value changes.
|
||||||
|
bool get mounted => _mounted;
|
||||||
|
bool _mounted = false;
|
||||||
|
void _updateMounted(bool value) {
|
||||||
|
if (value == _mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_mounted = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayState? _overlay;
|
||||||
|
final GlobalKey<_OverlayEntryWidgetState> _key =
|
||||||
|
GlobalKey<_OverlayEntryWidgetState>();
|
||||||
|
|
||||||
|
/// Remove this entry from the overlay.
|
||||||
|
///
|
||||||
|
/// This should only be called once.
|
||||||
|
///
|
||||||
|
/// This method removes this overlay entry from the overlay immediately. The
|
||||||
|
/// UI will be updated in the same frame if this method is called before the
|
||||||
|
/// overlay rebuild in this frame; otherwise, the UI will be updated in the
|
||||||
|
/// next frame. This means that it is safe to call during builds, but also
|
||||||
|
/// that if you do call this after the overlay rebuild, the UI will not update
|
||||||
|
/// until the next frame (i.e. many milliseconds later).
|
||||||
|
void remove() {
|
||||||
|
assert(_overlay != null);
|
||||||
|
final OverlayState overlay = _overlay!;
|
||||||
|
_overlay = null;
|
||||||
|
if (!overlay.mounted) return;
|
||||||
|
|
||||||
|
overlay._entries.remove(this);
|
||||||
|
if (SchedulerBinding.instance.schedulerPhase ==
|
||||||
|
SchedulerPhase.persistentCallbacks) {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||||
|
overlay._markDirty();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
overlay._markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cause this entry to rebuild during the next pipeline flush.
|
||||||
|
///
|
||||||
|
/// You need to call this function if the output of [builder] has changed.
|
||||||
|
void markNeedsBuild() {
|
||||||
|
_key.currentState?._markNeedsBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayEntryWidget extends StatefulWidget {
|
||||||
|
const _OverlayEntryWidget({
|
||||||
|
required Key key,
|
||||||
|
required this.entry,
|
||||||
|
this.tickerEnabled = true,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final OverlayEntry entry;
|
||||||
|
final bool tickerEnabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_OverlayEntryWidgetState createState() => _OverlayEntryWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.entry._updateMounted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.entry._updateMounted(false);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TickerMode(
|
||||||
|
enabled: widget.tickerEnabled,
|
||||||
|
child: widget.entry.builder(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _markNeedsBuild() {
|
||||||
|
setState(() {/* the state that changed is in the builder */});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stack of entries that can be managed independently.
|
||||||
|
///
|
||||||
|
/// Overlays let independent child widgets "float" visual elements on top of
|
||||||
|
/// other widgets by inserting them into the overlay's stack. The overlay lets
|
||||||
|
/// each of these widgets manage their participation in the overlay using
|
||||||
|
/// [OverlayEntry] objects.
|
||||||
|
///
|
||||||
|
/// Although you can create an [Overlay] directly, it's most common to use the
|
||||||
|
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
|
||||||
|
/// navigator uses its overlay to manage the visual appearance of its routes.
|
||||||
|
///
|
||||||
|
/// The [Overlay] widget uses a custom stack implementation, which is very
|
||||||
|
/// similar to the [Stack] widget. The main use case of [Overlay] is related to
|
||||||
|
/// navigation and being able to insert widgets on top of the pages in an app.
|
||||||
|
/// To simply display a stack of widgets, consider using [Stack] instead.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [OverlayEntry], the class that is used for describing the overlay entries.
|
||||||
|
/// * [OverlayState], which is used to insert the entries into the overlay.
|
||||||
|
/// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator].
|
||||||
|
/// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator].
|
||||||
|
/// * [Stack], which allows directly displaying a stack of widgets.
|
||||||
|
class Overlay extends StatefulWidget {
|
||||||
|
/// Creates an overlay.
|
||||||
|
///
|
||||||
|
/// The initial entries will be inserted into the overlay when its associated
|
||||||
|
/// [OverlayState] is initialized.
|
||||||
|
///
|
||||||
|
/// Rather than creating an overlay, consider using the overlay that is
|
||||||
|
/// created by the [Navigator] in a [WidgetsApp] or a [MaterialApp] for the application.
|
||||||
|
const Overlay({
|
||||||
|
Key? key,
|
||||||
|
this.initialEntries = const <OverlayEntry>[],
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The entries to include in the overlay initially.
|
||||||
|
///
|
||||||
|
/// These entries are only used when the [OverlayState] is initialized. If you
|
||||||
|
/// are providing a new [Overlay] description for an overlay that's already in
|
||||||
|
/// the tree, then the new entries are ignored.
|
||||||
|
///
|
||||||
|
/// To add entries to an [Overlay] that is already in the tree, use
|
||||||
|
/// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the
|
||||||
|
/// [Overlay] widget and obtain the [OverlayState] via
|
||||||
|
/// [GlobalKey.currentState]), and then use [OverlayState.insert] or
|
||||||
|
/// [OverlayState.insertAll].
|
||||||
|
///
|
||||||
|
/// To remove an entry from an [Overlay], use [OverlayEntry.remove].
|
||||||
|
final List<OverlayEntry> initialEntries;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// Defaults to [Clip.hardEdge], and must not be null.
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
|
/// The state from the closest instance of this class that encloses the given context.
|
||||||
|
///
|
||||||
|
/// In debug mode, if the `debugRequiredFor` argument is provided then this
|
||||||
|
/// function will assert that an overlay was found and will throw an exception
|
||||||
|
/// if not. The exception attempts to explain that the calling [Widget] (the
|
||||||
|
/// one given by the `debugRequiredFor` argument) needs an [Overlay] to be
|
||||||
|
/// present to function.
|
||||||
|
///
|
||||||
|
/// Typical usage is as follows:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// OverlayState overlay = Overlay.of(context);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If `rootOverlay` is set to true, the state from the furthest instance of
|
||||||
|
/// this class is given instead. Useful for installing overlay entries
|
||||||
|
/// above all subsequent instances of [Overlay].
|
||||||
|
///
|
||||||
|
/// This method can be expensive (it walks the element tree).
|
||||||
|
static OverlayState? of(
|
||||||
|
BuildContext context, {
|
||||||
|
bool rootOverlay = false,
|
||||||
|
Widget? debugRequiredFor,
|
||||||
|
}) {
|
||||||
|
final OverlayState? result = rootOverlay
|
||||||
|
? context.findRootAncestorStateOfType<OverlayState>()
|
||||||
|
: context.findAncestorStateOfType<OverlayState>();
|
||||||
|
assert(() {
|
||||||
|
if (debugRequiredFor != null && result == null) {
|
||||||
|
final List<DiagnosticsNode> information = <DiagnosticsNode>[
|
||||||
|
ErrorSummary('No Overlay widget found.'),
|
||||||
|
ErrorDescription(
|
||||||
|
'${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.'),
|
||||||
|
ErrorHint(
|
||||||
|
'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.'),
|
||||||
|
DiagnosticsProperty<Widget>(
|
||||||
|
'The specific widget that failed to find an overlay was',
|
||||||
|
debugRequiredFor,
|
||||||
|
style: DiagnosticsTreeStyle.errorProperty),
|
||||||
|
if (context.widget != debugRequiredFor)
|
||||||
|
context.describeElement(
|
||||||
|
'The context from which that widget was searching for an overlay was'),
|
||||||
|
];
|
||||||
|
|
||||||
|
throw FlutterError.fromParts(information);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
OverlayState createState() => OverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of an [Overlay].
|
||||||
|
///
|
||||||
|
/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
|
||||||
|
/// [insertAll] functions.
|
||||||
|
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
|
||||||
|
final List<OverlayEntry> _entries = <OverlayEntry>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
insertAll(widget.initialEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _insertionIndex(OverlayEntry? below, OverlayEntry? above) {
|
||||||
|
assert(above == null || below == null);
|
||||||
|
if (below != null) return _entries.indexOf(below);
|
||||||
|
if (above != null) return _entries.indexOf(above) + 1;
|
||||||
|
return _entries.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert the given entry into the overlay.
|
||||||
|
///
|
||||||
|
/// If `below` is non-null, the entry is inserted just below `below`.
|
||||||
|
/// If `above` is non-null, the entry is inserted just above `above`.
|
||||||
|
/// Otherwise, the entry is inserted on top.
|
||||||
|
///
|
||||||
|
/// It is an error to specify both `above` and `below`.
|
||||||
|
void insert(OverlayEntry entry, {OverlayEntry? below, OverlayEntry? above}) {
|
||||||
|
assert(_debugVerifyInsertPosition(above, below));
|
||||||
|
assert(!_entries.contains(entry),
|
||||||
|
'The specified entry is already present in the Overlay.');
|
||||||
|
assert(entry._overlay == null,
|
||||||
|
'The specified entry is already present in another Overlay.');
|
||||||
|
entry._overlay = this;
|
||||||
|
setState(() {
|
||||||
|
_entries.insert(_insertionIndex(below, above), entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert all the entries in the given iterable.
|
||||||
|
///
|
||||||
|
/// If `below` is non-null, the entries are inserted just below `below`.
|
||||||
|
/// If `above` is non-null, the entries are inserted just above `above`.
|
||||||
|
/// Otherwise, the entries are inserted on top.
|
||||||
|
///
|
||||||
|
/// It is an error to specify both `above` and `below`.
|
||||||
|
void insertAll(Iterable<OverlayEntry> entries,
|
||||||
|
{OverlayEntry? below, OverlayEntry? above}) {
|
||||||
|
assert(_debugVerifyInsertPosition(above, below));
|
||||||
|
assert(
|
||||||
|
entries.every((OverlayEntry entry) => !_entries.contains(entry)),
|
||||||
|
'One or more of the specified entries are already present in the Overlay.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
entries.every((OverlayEntry entry) => entry._overlay == null),
|
||||||
|
'One or more of the specified entries are already present in another Overlay.',
|
||||||
|
);
|
||||||
|
if (entries.isEmpty) return;
|
||||||
|
for (final OverlayEntry entry in entries) {
|
||||||
|
assert(entry._overlay == null);
|
||||||
|
entry._overlay = this;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_entries.insertAll(_insertionIndex(below, above), entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below,
|
||||||
|
{Iterable<OverlayEntry>? newEntries}) {
|
||||||
|
assert(
|
||||||
|
above == null || below == null,
|
||||||
|
'Only one of `above` and `below` may be specified.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
above == null ||
|
||||||
|
(above._overlay == this &&
|
||||||
|
_entries.contains(above) &&
|
||||||
|
(newEntries?.contains(above) ?? true)),
|
||||||
|
'The provided entry used for `above` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
below == null ||
|
||||||
|
(below._overlay == this &&
|
||||||
|
_entries.contains(below) &&
|
||||||
|
(newEntries?.contains(below) ?? true)),
|
||||||
|
'The provided entry used for `below` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all the entries listed in the given iterable, then reinsert them
|
||||||
|
/// into the overlay in the given order.
|
||||||
|
///
|
||||||
|
/// Entries mention in `newEntries` but absent from the overlay are inserted
|
||||||
|
/// as if with [insertAll].
|
||||||
|
///
|
||||||
|
/// Entries not mentioned in `newEntries` but present in the overlay are
|
||||||
|
/// positioned as a group in the resulting list relative to the entries that
|
||||||
|
/// were moved, as specified by one of `below` or `above`, which, if
|
||||||
|
/// specified, must be one of the entries in `newEntries`:
|
||||||
|
///
|
||||||
|
/// If `below` is non-null, the group is positioned just below `below`.
|
||||||
|
/// If `above` is non-null, the group is positioned just above `above`.
|
||||||
|
/// Otherwise, the group is left on top, with all the rearranged entries
|
||||||
|
/// below.
|
||||||
|
///
|
||||||
|
/// It is an error to specify both `above` and `below`.
|
||||||
|
void rearrange(Iterable<OverlayEntry> newEntries,
|
||||||
|
{OverlayEntry? below, OverlayEntry? above}) {
|
||||||
|
final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry>
|
||||||
|
? newEntries
|
||||||
|
: newEntries.toList(growable: false);
|
||||||
|
assert(
|
||||||
|
_debugVerifyInsertPosition(above, below, newEntries: newEntriesList));
|
||||||
|
assert(
|
||||||
|
newEntriesList.every((OverlayEntry entry) =>
|
||||||
|
entry._overlay == null || entry._overlay == this),
|
||||||
|
'One or more of the specified entries are already present in another Overlay.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
newEntriesList.every((OverlayEntry entry) =>
|
||||||
|
_entries.indexOf(entry) == _entries.lastIndexOf(entry)),
|
||||||
|
'One or more of the specified entries are specified multiple times.',
|
||||||
|
);
|
||||||
|
if (newEntriesList.isEmpty) return;
|
||||||
|
if (listEquals(_entries, newEntriesList)) return;
|
||||||
|
final LinkedHashSet<OverlayEntry> old =
|
||||||
|
LinkedHashSet<OverlayEntry>.of(_entries);
|
||||||
|
for (final OverlayEntry entry in newEntriesList) {
|
||||||
|
entry._overlay ??= this;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_entries.clear();
|
||||||
|
_entries.addAll(newEntriesList);
|
||||||
|
old.removeAll(newEntriesList);
|
||||||
|
_entries.insertAll(_insertionIndex(below, above), old);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _markDirty() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
|
||||||
|
/// opaque entry).
|
||||||
|
///
|
||||||
|
/// This is an O(N) algorithm, and should not be necessary except for debug
|
||||||
|
/// asserts. To avoid people depending on it, this function is implemented
|
||||||
|
/// only in debug mode, and always returns false in release mode.
|
||||||
|
bool debugIsVisible(OverlayEntry entry) {
|
||||||
|
bool result = false;
|
||||||
|
assert(_entries.contains(entry));
|
||||||
|
assert(() {
|
||||||
|
for (int i = _entries.length - 1; i > 0; i -= 1) {
|
||||||
|
final OverlayEntry candidate = _entries[i];
|
||||||
|
if (candidate == entry) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (candidate.opaque) break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _didChangeEntryOpacity() {
|
||||||
|
setState(() {
|
||||||
|
// We use the opacity of the entry in our build function, which means we
|
||||||
|
// our state has changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This list is filled backwards and then reversed below before
|
||||||
|
// it is added to the tree.
|
||||||
|
final List<Widget> children = <Widget>[];
|
||||||
|
bool onstage = true;
|
||||||
|
int onstageCount = 0;
|
||||||
|
for (int i = _entries.length - 1; i >= 0; i -= 1) {
|
||||||
|
final OverlayEntry entry = _entries[i];
|
||||||
|
if (onstage) {
|
||||||
|
onstageCount += 1;
|
||||||
|
children.add(_OverlayEntryWidget(
|
||||||
|
key: entry._key,
|
||||||
|
entry: entry,
|
||||||
|
));
|
||||||
|
if (entry.opaque) onstage = false;
|
||||||
|
} else if (entry.maintainState) {
|
||||||
|
children.add(_OverlayEntryWidget(
|
||||||
|
key: entry._key,
|
||||||
|
entry: entry,
|
||||||
|
tickerEnabled: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _Theatre(
|
||||||
|
skipCount: children.length - onstageCount,
|
||||||
|
clipBehavior: widget.clipBehavior,
|
||||||
|
children: children.reversed.toList(growable: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
// TODO(jacobr): use IterableProperty instead as that would
|
||||||
|
// provide a slightly more consistent string summary of the List.
|
||||||
|
properties
|
||||||
|
.add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Special version of a [Stack], that doesn't layout and render the first
|
||||||
|
/// [skipCount] children.
|
||||||
|
///
|
||||||
|
/// The first [skipCount] children are considered "offstage".
|
||||||
|
class _Theatre extends MultiChildRenderObjectWidget {
|
||||||
|
_Theatre({
|
||||||
|
Key? key,
|
||||||
|
this.skipCount = 0,
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
|
List<Widget> children = const <Widget>[],
|
||||||
|
}) : assert(skipCount >= 0),
|
||||||
|
assert(children.length >= skipCount),
|
||||||
|
super(key: key, children: children);
|
||||||
|
|
||||||
|
final int skipCount;
|
||||||
|
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TheatreElement createElement() => _TheatreElement(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RenderTheatre createRenderObject(BuildContext context) {
|
||||||
|
return _RenderTheatre(
|
||||||
|
skipCount: skipCount,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, _RenderTheatre renderObject) {
|
||||||
|
renderObject
|
||||||
|
..skipCount = skipCount
|
||||||
|
..textDirection = Directionality.of(context)
|
||||||
|
..clipBehavior = clipBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(IntProperty('skipCount', skipCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TheatreElement extends MultiChildRenderObjectElement {
|
||||||
|
_TheatreElement(_Theatre widget) : super(widget);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugVisitOnstageChildren(ElementVisitor visitor) {
|
||||||
|
final _Theatre theatre = widget as _Theatre;
|
||||||
|
assert(children.length >= theatre.skipCount);
|
||||||
|
children.skip(theatre.skipCount).forEach(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderTheatre extends RenderBox
|
||||||
|
with ContainerRenderObjectMixin<RenderBox, StackParentData> {
|
||||||
|
_RenderTheatre({
|
||||||
|
List<RenderBox>? children,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
int skipCount = 0,
|
||||||
|
Clip clipBehavior = Clip.hardEdge,
|
||||||
|
}) : assert(skipCount >= 0),
|
||||||
|
_textDirection = textDirection,
|
||||||
|
_skipCount = skipCount,
|
||||||
|
_clipBehavior = clipBehavior {
|
||||||
|
addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasVisualOverflow = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! StackParentData) {
|
||||||
|
child.parentData = StackParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Alignment? _resolvedAlignment;
|
||||||
|
|
||||||
|
void _resolve() {
|
||||||
|
if (_resolvedAlignment != null) return;
|
||||||
|
_resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _markNeedResolution() {
|
||||||
|
_resolvedAlignment = null;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextDirection get textDirection => _textDirection;
|
||||||
|
TextDirection _textDirection;
|
||||||
|
set textDirection(TextDirection value) {
|
||||||
|
if (_textDirection == value) return;
|
||||||
|
_textDirection = value;
|
||||||
|
_markNeedResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get skipCount => _skipCount;
|
||||||
|
int _skipCount;
|
||||||
|
set skipCount(int value) {
|
||||||
|
if (_skipCount != value) {
|
||||||
|
_skipCount = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// Defaults to [Clip.hardEdge], and must not be null.
|
||||||
|
Clip get clipBehavior => _clipBehavior;
|
||||||
|
Clip _clipBehavior = Clip.hardEdge;
|
||||||
|
set clipBehavior(Clip value) {
|
||||||
|
if (value != _clipBehavior) {
|
||||||
|
_clipBehavior = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
markNeedsSemanticsUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBox? get _firstOnstageChild {
|
||||||
|
if (skipCount == super.childCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
RenderBox? child = super.firstChild;
|
||||||
|
for (int toSkip = skipCount; toSkip > 0; toSkip--) {
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child!.parentData! as StackParentData;
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
assert(child != null);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBox? get _lastOnstageChild =>
|
||||||
|
skipCount == super.childCount ? null : lastChild;
|
||||||
|
|
||||||
|
int get _onstageChildCount => childCount - skipCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicWidth(double height) {
|
||||||
|
return RenderStack.getIntrinsicDimension(_firstOnstageChild,
|
||||||
|
(RenderBox child) => child.getMinIntrinsicWidth(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicWidth(double height) {
|
||||||
|
return RenderStack.getIntrinsicDimension(_firstOnstageChild,
|
||||||
|
(RenderBox child) => child.getMaxIntrinsicWidth(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicHeight(double width) {
|
||||||
|
return RenderStack.getIntrinsicDimension(_firstOnstageChild,
|
||||||
|
(RenderBox child) => child.getMinIntrinsicHeight(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicHeight(double width) {
|
||||||
|
return RenderStack.getIntrinsicDimension(_firstOnstageChild,
|
||||||
|
(RenderBox child) => child.getMaxIntrinsicHeight(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
|
assert(!debugNeedsLayout);
|
||||||
|
double? result;
|
||||||
|
RenderBox? child = _firstOnstageChild;
|
||||||
|
while (child != null) {
|
||||||
|
assert(!child.debugNeedsLayout);
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child.parentData! as StackParentData;
|
||||||
|
double? candidate = child.getDistanceToActualBaseline(baseline);
|
||||||
|
if (candidate != null) {
|
||||||
|
candidate += childParentData.offset.dy;
|
||||||
|
if (result != null) {
|
||||||
|
result = math.min(result, candidate);
|
||||||
|
} else {
|
||||||
|
result = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get sizedByParent => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size computeDryLayout(BoxConstraints constraints) {
|
||||||
|
assert(constraints.biggest.isFinite);
|
||||||
|
return constraints.biggest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
_hasVisualOverflow = false;
|
||||||
|
|
||||||
|
if (_onstageChildCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolve();
|
||||||
|
assert(_resolvedAlignment != null);
|
||||||
|
|
||||||
|
// Same BoxConstraints as used by RenderStack for StackFit.expand.
|
||||||
|
final BoxConstraints nonPositionedConstraints =
|
||||||
|
BoxConstraints.tight(constraints.biggest);
|
||||||
|
|
||||||
|
RenderBox? child = _firstOnstageChild;
|
||||||
|
while (child != null) {
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child.parentData! as StackParentData;
|
||||||
|
|
||||||
|
if (!childParentData.isPositioned) {
|
||||||
|
child.layout(nonPositionedConstraints, parentUsesSize: true);
|
||||||
|
childParentData.offset =
|
||||||
|
_resolvedAlignment!.alongOffset(size - child.size as Offset);
|
||||||
|
} else {
|
||||||
|
_hasVisualOverflow = RenderStack.layoutPositionedChild(
|
||||||
|
child, childParentData, size, _resolvedAlignment!) ||
|
||||||
|
_hasVisualOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(child.parentData == childParentData);
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
RenderBox? child = _lastOnstageChild;
|
||||||
|
for (int i = 0; i < _onstageChildCount; i++) {
|
||||||
|
assert(child != null);
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child!.parentData! as StackParentData;
|
||||||
|
final bool isHit = result.addWithPaintOffset(
|
||||||
|
offset: childParentData.offset,
|
||||||
|
position: position,
|
||||||
|
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||||
|
assert(transformed == position - childParentData.offset);
|
||||||
|
return child!.hitTest(result, position: transformed);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (isHit) return true;
|
||||||
|
child = childParentData.previousSibling;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void paintStack(PaintingContext context, Offset offset) {
|
||||||
|
RenderBox? child = _firstOnstageChild;
|
||||||
|
while (child != null) {
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child.parentData! as StackParentData;
|
||||||
|
context.paintChild(child, childParentData.offset + offset);
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
_hasVisualOverflow = true;
|
||||||
|
if (_hasVisualOverflow && clipBehavior != Clip.none) {
|
||||||
|
_clipRectLayer.layer = context.pushClipRect(
|
||||||
|
needsCompositing,
|
||||||
|
offset,
|
||||||
|
Offset.zero & size,
|
||||||
|
paintStack,
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
oldLayer: _clipRectLayer.layer,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_clipRectLayer.layer = null;
|
||||||
|
paintStack(context, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final LayerHandle<ClipRectLayer> _clipRectLayer =
|
||||||
|
LayerHandle<ClipRectLayer>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_clipRectLayer.layer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||||
|
RenderBox? child = _firstOnstageChild;
|
||||||
|
while (child != null) {
|
||||||
|
visitor(child);
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child.parentData! as StackParentData;
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect? describeApproximatePaintClip(RenderObject child) =>
|
||||||
|
_hasVisualOverflow ? Offset.zero & size : null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(IntProperty('skipCount', skipCount));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<DiagnosticsNode> debugDescribeChildren() {
|
||||||
|
final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[];
|
||||||
|
final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[];
|
||||||
|
|
||||||
|
int count = 1;
|
||||||
|
bool onstage = false;
|
||||||
|
RenderBox? child = firstChild;
|
||||||
|
final RenderBox? firstOnstageChild = _firstOnstageChild;
|
||||||
|
while (child != null) {
|
||||||
|
if (child == firstOnstageChild) {
|
||||||
|
onstage = true;
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onstage) {
|
||||||
|
onstageChildren.add(
|
||||||
|
child.toDiagnosticsNode(
|
||||||
|
name: 'onstage $count',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
offstageChildren.add(
|
||||||
|
child.toDiagnosticsNode(
|
||||||
|
name: 'offstage $count',
|
||||||
|
style: DiagnosticsTreeStyle.offstage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final StackParentData childParentData =
|
||||||
|
child.parentData! as StackParentData;
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DiagnosticsNode>[
|
||||||
|
...onstageChildren,
|
||||||
|
if (offstageChildren.isNotEmpty)
|
||||||
|
...offstageChildren
|
||||||
|
else
|
||||||
|
DiagnosticsNode.message(
|
||||||
|
'no offstage children',
|
||||||
|
style: DiagnosticsTreeStyle.offstage,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
@ -185,12 +183,12 @@ extension on EditorState {
|
|||||||
}
|
}
|
||||||
final imageNode = Node(
|
final imageNode = Node(
|
||||||
type: 'image',
|
type: 'image',
|
||||||
children: LinkedList(),
|
|
||||||
attributes: {
|
attributes: {
|
||||||
'image_src': src,
|
'image_src': src,
|
||||||
'align': 'center',
|
'align': 'center',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
final transaction = this.transaction;
|
||||||
transaction.insertNode(
|
transaction.insertNode(
|
||||||
selection.start.path,
|
selection.start.path,
|
||||||
imageNode,
|
imageNode,
|
||||||
|
@ -26,7 +26,7 @@ class FlowyRichText extends StatefulWidget {
|
|||||||
const FlowyRichText({
|
const FlowyRichText({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.cursorHeight,
|
this.cursorHeight,
|
||||||
this.cursorWidth = 1.0,
|
this.cursorWidth = 1.5,
|
||||||
this.lineHeight = 1.0,
|
this.lineHeight = 1.0,
|
||||||
this.textSpanDecorator,
|
this.textSpanDecorator,
|
||||||
this.placeholderText = ' ',
|
this.placeholderText = ' ',
|
||||||
@ -55,7 +55,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
RenderParagraph get _renderParagraph =>
|
RenderParagraph get _renderParagraph =>
|
||||||
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||||
|
|
||||||
RenderParagraph get _placeholderRenderParagraph =>
|
RenderParagraph? get _placeholderRenderParagraph =>
|
||||||
_placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph;
|
_placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -79,7 +79,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Position end() => Position(
|
Position end() => Position(
|
||||||
path: widget.textNode.path, offset: widget.textNode.delta.length);
|
path: widget.textNode.path, offset: widget.textNode.toPlainText().length);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect? getCursorRectInPosition(Position position) {
|
Rect? getCursorRectInPosition(Position position) {
|
||||||
@ -90,12 +90,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
||||||
if (cursorHeight == null) {
|
if (cursorHeight == null) {
|
||||||
cursorHeight =
|
cursorHeight =
|
||||||
_placeholderRenderParagraph.getFullHeightForCaret(textPosition);
|
_placeholderRenderParagraph?.getFullHeightForCaret(textPosition);
|
||||||
cursorOffset = _placeholderRenderParagraph.getOffsetForCaret(
|
cursorOffset = _placeholderRenderParagraph?.getOffsetForCaret(
|
||||||
textPosition, Rect.zero);
|
textPosition, Rect.zero) ??
|
||||||
|
Offset.zero;
|
||||||
}
|
}
|
||||||
final rect = Rect.fromLTWH(
|
final rect = Rect.fromLTWH(
|
||||||
cursorOffset.dx - (widget.cursorWidth / 2),
|
cursorOffset.dx - (widget.cursorWidth / 2.0),
|
||||||
cursorOffset.dy,
|
cursorOffset.dy,
|
||||||
widget.cursorWidth,
|
widget.cursorWidth,
|
||||||
widget.cursorHeight ?? cursorHeight ?? 16.0,
|
widget.cursorHeight ?? cursorHeight ?? 16.0,
|
||||||
@ -297,6 +298,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
|||||||
|
|
||||||
timer = Timer(const Duration(milliseconds: 200), () {
|
timer = Timer(const Duration(milliseconds: 200), () {
|
||||||
tapCount = 0;
|
tapCount = 0;
|
||||||
|
widget.editorState.service.selectionService
|
||||||
|
.updateSelection(selection);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
showLinkMenu(
|
showLinkMenu(
|
||||||
context,
|
context,
|
||||||
|
@ -59,10 +59,16 @@ class SelectionMenu implements SelectionMenuService {
|
|||||||
// Workaround: We can customize the padding through the [EditorStyle],
|
// Workaround: We can customize the padding through the [EditorStyle],
|
||||||
// but the coordinates of overlay are not properly converted currently.
|
// but the coordinates of overlay are not properly converted currently.
|
||||||
// Just subtract the padding here as a result.
|
// Just subtract the padding here as a result.
|
||||||
|
const menuHeight = 200.0;
|
||||||
|
const menuOffset = Offset(10, 10);
|
||||||
final baseOffset =
|
final baseOffset =
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
final offset =
|
var offset = selectionRects.first.bottomRight + menuOffset;
|
||||||
selectionRects.first.bottomRight + const Offset(10, 10) - baseOffset;
|
if (offset.dy >=
|
||||||
|
baseOffset.dy + editorState.renderBox!.size.height - menuHeight) {
|
||||||
|
offset = selectionRects.first.topRight - menuOffset;
|
||||||
|
offset = offset.translate(0, -menuHeight);
|
||||||
|
}
|
||||||
_topLeft = offset;
|
_topLeft = offset;
|
||||||
|
|
||||||
_selectionMenuEntry = OverlayEntry(builder: (context) {
|
_selectionMenuEntry = OverlayEntry(builder: (context) {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/commands/text/text_commands.dart';
|
import 'package:appflowy_editor/src/commands/text/text_commands.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
|
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
|
||||||
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||||
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
|
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
|
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||||
|
|
||||||
typedef ToolbarItemEventHandler = void Function(
|
typedef ToolbarItemEventHandler = void Function(
|
||||||
@ -206,7 +207,9 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
BuiltInAttributeKey.subtype,
|
BuiltInAttributeKey.subtype,
|
||||||
(value) => value == BuiltInAttributeKey.quote,
|
(value) => value == BuiltInAttributeKey.quote,
|
||||||
),
|
),
|
||||||
handler: (editorState, context) => formatQuote(editorState),
|
handler: (editorState, context) {
|
||||||
|
formatQuote(editorState);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ToolbarItem(
|
ToolbarItem(
|
||||||
id: 'appflowy.toolbar.bulleted_list',
|
id: 'appflowy.toolbar.bulleted_list',
|
||||||
|
@ -25,7 +25,7 @@ class ToolbarItemWidget extends StatelessWidget {
|
|||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
highlightColor: Colors.yellow,
|
highlightColor: Colors.transparent,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: item.iconBuilder(isHighlight),
|
icon: item.iconBuilder(isHighlight),
|
||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
|
|||||||
isHighlight: item.highlightCallback(widget.editorState),
|
isHighlight: item.highlightCallback(widget.editorState),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
item.handler(widget.editorState, context);
|
item.handler(widget.editorState, context);
|
||||||
|
widget.editorState.service.keyboardService?.enable();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||||
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
import 'package:appflowy_editor/src/render/editor/editor_entry.dart';
|
import 'package:appflowy_editor/src/render/editor/editor_entry.dart';
|
||||||
|
@ -97,6 +97,7 @@ void _pasteHTML(EditorState editorState, String html) {
|
|||||||
final firstTextNode = firstNode as TextNode;
|
final firstTextNode = firstNode as TextNode;
|
||||||
tb.updateText(
|
tb.updateText(
|
||||||
textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
|
textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
|
||||||
|
tb.updateNode(textNodeAtPath, firstTextNode.attributes);
|
||||||
tb.afterSelection = (Selection.collapsed(Position(
|
tb.afterSelection = (Selection.collapsed(Position(
|
||||||
path: path, offset: startOffset + firstTextNode.delta.length)));
|
path: path, offset: startOffset + firstTextNode.delta.length)));
|
||||||
editorState.apply(tb);
|
editorState.apply(tb);
|
||||||
@ -114,7 +115,7 @@ void _pasteMultipleLinesInText(
|
|||||||
final firstNode = nodes[0];
|
final firstNode = nodes[0];
|
||||||
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||||
|
|
||||||
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
if (nodeAtPath.type == 'text' && firstNode.type == 'text') {
|
||||||
int? startNumber;
|
int? startNumber;
|
||||||
if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) {
|
if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) {
|
||||||
startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int;
|
startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int;
|
||||||
@ -131,6 +132,7 @@ void _pasteMultipleLinesInText(
|
|||||||
..retain(offset)
|
..retain(offset)
|
||||||
..delete(remain.length)) +
|
..delete(remain.length)) +
|
||||||
firstTextNode.delta);
|
firstTextNode.delta);
|
||||||
|
tb.updateNode(textNodeAtPath, firstTextNode.attributes);
|
||||||
|
|
||||||
final tailNodes = nodes.sublist(1);
|
final tailNodes = nodes.sublist(1);
|
||||||
final originalPath = [...path];
|
final originalPath = [...path];
|
||||||
|
@ -393,3 +393,48 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
|
|||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
|
||||||
|
// Obtain the selection and selected nodes of the current document through the 'selectionService'
|
||||||
|
// to determine whether the selection is collapsed and whether the selected node is a text node.
|
||||||
|
final selectionService = editorState.service.selectionService;
|
||||||
|
final selection = selectionService.currentSelection.value;
|
||||||
|
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
|
||||||
|
if (selection == null || !selection.isSingle || textNodes.length != 1) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
final text = textNode.toPlainText();
|
||||||
|
// Determine if an 'underscore' already exists in the text node and only once.
|
||||||
|
final firstUnderscore = text.indexOf('_');
|
||||||
|
final lastUnderscore = text.lastIndexOf('_');
|
||||||
|
if (firstUnderscore == -1 ||
|
||||||
|
firstUnderscore != lastUnderscore ||
|
||||||
|
firstUnderscore == selection.start.offset - 1) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the previous 'underscore',
|
||||||
|
// update the style of the text surrounded by the two underscores to 'italic',
|
||||||
|
// and update the cursor position.
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..deleteText(textNode, firstUnderscore, 1)
|
||||||
|
..formatText(
|
||||||
|
textNode,
|
||||||
|
firstUnderscore,
|
||||||
|
selection.end.offset - firstUnderscore - 1,
|
||||||
|
{
|
||||||
|
BuiltInAttributeKey.italic: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: selection.end.offset - 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
@ -189,7 +189,8 @@ KeyEventResult _toHeadingStyle(
|
|||||||
|
|
||||||
int _countOfSign(String text, Selection selection) {
|
int _countOfSign(String text, Selection selection) {
|
||||||
for (var i = 6; i >= 0; i--) {
|
for (var i = 6; i >= 0; i--) {
|
||||||
if (text.substring(0, selection.end.offset).contains('#' * i)) {
|
final heading = text.substring(0, selection.end.offset);
|
||||||
|
if (heading.contains('#' * i) && heading.length == i) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,11 +92,16 @@ class _AppFlowyScrollState extends State<AppFlowyScroll>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerSignal: _onPointerSignal,
|
onPointerSignal: _onPointerSignal,
|
||||||
child: SingleChildScrollView(
|
child: CustomScrollView(
|
||||||
key: _scrollViewKey,
|
key: _scrollViewKey,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/infra/log.dart';
|
import 'package:appflowy_editor/src/infra/log.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/core/document/node_iterator.dart';
|
import 'package:appflowy_editor/src/core/document/node_iterator.dart';
|
||||||
@ -505,9 +506,14 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showContextMenu(TapDownDetails details) {
|
void _showContextMenu(TapDownDetails details) {
|
||||||
|
_clearContextMenu();
|
||||||
|
|
||||||
|
final baseOffset =
|
||||||
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
|
final offset = details.globalPosition + const Offset(10, 10) - baseOffset;
|
||||||
final contextMenu = OverlayEntry(
|
final contextMenu = OverlayEntry(
|
||||||
builder: (context) => ContextMenu(
|
builder: (context) => ContextMenu(
|
||||||
position: details.globalPosition,
|
position: offset,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
items: builtInContextMenuItems,
|
items: builtInContextMenuItems,
|
||||||
onPressed: () => _clearContextMenu(),
|
onPressed: () => _clearContextMenu(),
|
||||||
|
@ -285,6 +285,11 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
|||||||
command: 'escape',
|
command: 'escape',
|
||||||
handler: exitEditingModeEventHandler,
|
handler: exitEditingModeEventHandler,
|
||||||
),
|
),
|
||||||
|
ShortcutEvent(
|
||||||
|
key: 'Underscore to italic',
|
||||||
|
command: 'shift+underscore',
|
||||||
|
handler: underscoreToItalicHandler,
|
||||||
|
),
|
||||||
// https://github.com/flutter/flutter/issues/104944
|
// https://github.com/flutter/flutter/issues/104944
|
||||||
// Workaround: Using space editing on the web platform often results in errors,
|
// Workaround: Using space editing on the web platform often results in errors,
|
||||||
// so adding a shortcut event to handle the space input instead of using the
|
// so adding a shortcut event to handle the space input instead of using the
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import '../../infra/test_editor.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
@ -39,5 +41,34 @@ void main() async {
|
|||||||
|
|
||||||
expect(submittedText, link);
|
expect(submittedText, link);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('test tap linked text', (tester) async {
|
||||||
|
const link = 'appflowy.io';
|
||||||
|
// This is a link [appflowy.io](appflowy.io)
|
||||||
|
final editor = tester.editor
|
||||||
|
..insertTextNode(
|
||||||
|
null,
|
||||||
|
delta: Delta()
|
||||||
|
..insert(
|
||||||
|
'appflowy.io',
|
||||||
|
attributes: {
|
||||||
|
BuiltInAttributeKey.href: link,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await editor.startTesting();
|
||||||
|
final finder = find.byType(RichText);
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
|
||||||
|
// tap the link
|
||||||
|
await editor.updateSelection(
|
||||||
|
Selection.single(path: [0], startOffset: 0, endOffset: link.length),
|
||||||
|
);
|
||||||
|
await tester.tap(finder);
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 350));
|
||||||
|
final linkMenu = find.byType(LinkMenu);
|
||||||
|
expect(linkMenu, findsOneWidget);
|
||||||
|
expect(find.text(link, findRichText: true), findsNWidgets(2));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -213,5 +213,19 @@ void main() async {
|
|||||||
expect(textNode.attributes.check, true);
|
expect(textNode.attributes.check, true);
|
||||||
expect(textNode.toPlainText(), insertedText);
|
expect(textNode.toPlainText(), insertedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Presses # at the end of the text', (tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁 #';
|
||||||
|
final editor = tester.editor..insertTextNode(text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final textNode = editor.nodeAtPath([0]) as TextNode;
|
||||||
|
await editor.updateSelection(
|
||||||
|
Selection.single(path: [0], startOffset: text.length),
|
||||||
|
);
|
||||||
|
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||||
|
expect(textNode.subtype, null);
|
||||||
|
expect(textNode.toPlainText(), text);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import 'package:flowy_sdk/protobuf/dart-ffi/protobuf.dart';
|
|||||||
import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-sync/protobuf.dart';
|
|
||||||
|
|
||||||
// ignore: unused_import
|
// ignore: unused_import
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
@ -586,6 +586,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -91,6 +91,7 @@ dependencies:
|
|||||||
bloc: ^8.1.0
|
bloc: ^8.1.0
|
||||||
textstyle_extensions: "2.0.0-nullsafety"
|
textstyle_extensions: "2.0.0-nullsafety"
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.0.15
|
||||||
|
google_fonts: ^3.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
@ -129,6 +130,26 @@ flutter:
|
|||||||
- family: FlowyIconData
|
- family: FlowyIconData
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/FlowyIconData.ttf
|
- asset: assets/fonts/FlowyIconData.ttf
|
||||||
|
- family: Poppins
|
||||||
|
fonts:
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf
|
||||||
|
weight: 100
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Thin.ttf
|
||||||
|
weight: 200
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Light.ttf
|
||||||
|
weight: 300
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Regular.ttf
|
||||||
|
weight: 400
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Medium.ttf
|
||||||
|
weight: 500
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-SemiBold.ttf
|
||||||
|
weight: 600
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Bold.ttf
|
||||||
|
weight: 700
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-Black.ttf
|
||||||
|
weight: 800
|
||||||
|
- asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf
|
||||||
|
weight: 900
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
|
@ -30,7 +30,7 @@ class AppFlowyGridTest {
|
|||||||
final result = await AppService().createView(
|
final result = await AppService().createView(
|
||||||
appId: app.id,
|
appId: app.id,
|
||||||
name: "Test Grid",
|
name: "Test Grid",
|
||||||
dataType: builder.dataType,
|
dataFormatType: builder.dataFormatType,
|
||||||
pluginType: builder.pluginType,
|
pluginType: builder.pluginType,
|
||||||
layoutType: builder.layoutType!,
|
layoutType: builder.layoutType!,
|
||||||
);
|
);
|
||||||
|
2
frontend/rust-lib/Cargo.lock
generated
2
frontend/rust-lib/Cargo.lock
generated
@ -871,6 +871,7 @@ dependencies = [
|
|||||||
"lib-ot",
|
"lib-ot",
|
||||||
"lib-ws",
|
"lib-ws",
|
||||||
"log",
|
"log",
|
||||||
|
"md5",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1773,6 +1774,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"indexmap",
|
||||||
"indextree",
|
"indextree",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -23,7 +23,7 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
|
|||||||
let path: &str = c_str.to_str().unwrap();
|
let path: &str = c_str.to_str().unwrap();
|
||||||
|
|
||||||
let server_config = get_client_server_configuration().unwrap();
|
let server_config = get_client_server_configuration().unwrap();
|
||||||
let config = FlowySDKConfig::new(path, "appflowy", server_config, false).log_filter("info");
|
let config = FlowySDKConfig::new(path, "appflowy", server_config).log_filter("info");
|
||||||
FLOWY_SDK.get_or_init(|| FlowySDK::new(config));
|
FLOWY_SDK.get_or_init(|| FlowySDK::new(config));
|
||||||
|
|
||||||
0
|
0
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE grid_view_rev_table;
|
@ -0,0 +1,9 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE document_rev_table (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
document_id TEXT NOT NULL DEFAULT '',
|
||||||
|
base_rev_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
rev_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
data BLOB NOT NULL DEFAULT (x''),
|
||||||
|
state INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
@ -13,6 +13,17 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
document_rev_table (id) {
|
||||||
|
id -> Integer,
|
||||||
|
document_id -> Text,
|
||||||
|
base_rev_id -> BigInt,
|
||||||
|
rev_id -> BigInt,
|
||||||
|
data -> Binary,
|
||||||
|
state -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
grid_block_index_table (row_id) {
|
grid_block_index_table (row_id) {
|
||||||
row_id -> Text,
|
row_id -> Text,
|
||||||
@ -133,6 +144,7 @@ table! {
|
|||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
app_table,
|
app_table,
|
||||||
|
document_rev_table,
|
||||||
grid_block_index_table,
|
grid_block_index_table,
|
||||||
grid_meta_rev_table,
|
grid_meta_rev_table,
|
||||||
grid_rev_table,
|
grid_rev_table,
|
||||||
|
@ -28,6 +28,7 @@ tokio = {version = "1", features = ["sync"]}
|
|||||||
tracing = { version = "0.1", features = ["log"] }
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
|
|
||||||
bytes = { version = "1.1" }
|
bytes = { version = "1.1" }
|
||||||
|
md5 = "0.7.0"
|
||||||
strum = "0.21"
|
strum = "0.21"
|
||||||
strum_macros = "0.21"
|
strum_macros = "0.21"
|
||||||
dashmap = "5"
|
dashmap = "5"
|
||||||
|
419
frontend/rust-lib/flowy-document/src/editor/READ_ME.json
Normal file
419
frontend/rust-lib/flowy-document/src/editor/READ_ME.json
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
{
|
||||||
|
"document": {
|
||||||
|
"type": "editor",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"heading": "h1"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "🌟 Welcome to AppFlowy!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Here are the basics"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Click anywhere and just start typing."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Highlight",
|
||||||
|
"attributes": {
|
||||||
|
"backgroundColor": "0x6000BCF0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " any text, and use the editing menu to "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "style",
|
||||||
|
"attributes": {
|
||||||
|
"italic": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "your",
|
||||||
|
"attributes": {
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "writing",
|
||||||
|
"attributes": {
|
||||||
|
"underline": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "however",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " you "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "like.",
|
||||||
|
"attributes": {
|
||||||
|
"strikethrough": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "As soon as you type "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "/",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " a menu will pop up. Select different types of content blocks you can add."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Type "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "/",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " followed by "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "/bullet",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " or "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "/c.",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": true
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Click "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "+ New Page ",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "button at the bottom of your sidebar to add a new page."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "checkbox",
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Click "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "+",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " next to any page title in the sidebar to quickly add a new subpage."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"checkbox": null,
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Markdown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 1,
|
||||||
|
"heading": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Heading "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 2
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "bold text",
|
||||||
|
"attributes": {
|
||||||
|
"bold": true,
|
||||||
|
"defaultFormating": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 3
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "italicized text",
|
||||||
|
"attributes": {
|
||||||
|
"italic": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "number-list",
|
||||||
|
"number": 4,
|
||||||
|
"number-list": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Ordered List"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"number": 5,
|
||||||
|
"subtype": "number-list"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "code",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"number": 6,
|
||||||
|
"subtype": "number-list"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Strikethrough",
|
||||||
|
"attributes": {
|
||||||
|
"strikethrough": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"retain": 1,
|
||||||
|
"attributes": {
|
||||||
|
"strikethrough": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"checkbox": null
|
||||||
|
},
|
||||||
|
"delta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"checkbox": null,
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Have a question?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "quote"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Click "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "?",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " at the bottom right for help and support."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "heading",
|
||||||
|
"heading": "h2"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Like AppFlowy? Follow us:"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "bulleted-list",
|
||||||
|
"quote": null
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "GitHub",
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "bulleted-list"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Twitter: @appflowy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": "bulleted-list"
|
||||||
|
},
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "Newsletter",
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://blog-appflowy.ghost.io/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"subtype": null,
|
||||||
|
"heading": null
|
||||||
|
},
|
||||||
|
"delta": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_revision::{RevisionObjectDeserializer, RevisionObjectSerializer};
|
use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer};
|
||||||
use flowy_sync::entities::revision::Revision;
|
use flowy_sync::entities::revision::Revision;
|
||||||
use lib_ot::core::{
|
use lib_ot::core::{
|
||||||
Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
|
Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
|
||||||
};
|
};
|
||||||
use lib_ot::text_delta::TextOperationBuilder;
|
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
@ -30,6 +30,11 @@ impl Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn md5(&self) -> String {
|
||||||
|
// format!("{:x}", md5::compute(bytes))
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_tree(&self) -> &NodeTree {
|
pub fn get_tree(&self) -> &NodeTree {
|
||||||
&self.tree
|
&self.tree
|
||||||
}
|
}
|
||||||
@ -40,7 +45,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn initial_document_content() -> String {
|
pub fn initial_document_content() -> String {
|
||||||
let delta = TextOperationBuilder::new().insert("").build();
|
let delta = DeltaTextOperationBuilder::new().insert("").build();
|
||||||
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
|
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
|
||||||
let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
|
let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
|
||||||
let node_operation = NodeOperation::Insert {
|
let node_operation = NodeOperation::Insert {
|
||||||
@ -78,7 +83,7 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
|
|||||||
|
|
||||||
fn deserialize_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
|
fn deserialize_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
|
||||||
let mut tree = NodeTree::new(make_tree_context());
|
let mut tree = NodeTree::new(make_tree_context());
|
||||||
let transaction = make_transaction_from_revisions(revisions)?;
|
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||||
let _ = tree.apply_transaction(transaction)?;
|
let _ = tree.apply_transaction(transaction)?;
|
||||||
let document = Document::new(tree);
|
let document = Document::new(tree);
|
||||||
Result::<Document, FlowyError>::Ok(document)
|
Result::<Document, FlowyError>::Ok(document)
|
||||||
@ -87,12 +92,20 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
|
|||||||
|
|
||||||
impl RevisionObjectSerializer for DocumentRevisionSerde {
|
impl RevisionObjectSerializer for DocumentRevisionSerde {
|
||||||
fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||||
let transaction = make_transaction_from_revisions(revisions)?;
|
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||||
Ok(Bytes::from(transaction.to_bytes()?))
|
Ok(Bytes::from(transaction.to_bytes()?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_transaction_from_revisions(revisions: Vec<Revision>) -> FlowyResult<Transaction> {
|
pub(crate) struct DocumentRevisionCompress();
|
||||||
|
impl RevisionCompress for DocumentRevisionCompress {
|
||||||
|
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||||
|
DocumentRevisionSerde::combine_revisions(revisions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||||
|
pub fn make_transaction_from_revisions(revisions: &[Revision]) -> FlowyResult<Transaction> {
|
||||||
let mut transaction = Transaction::new();
|
let mut transaction = Transaction::new();
|
||||||
for revision in revisions {
|
for revision in revisions {
|
||||||
let _ = transaction.compose(Transaction::from_bytes(&revision.bytes)?)?;
|
let _ = transaction.compose(Transaction::from_bytes(&revision.bytes)?)?;
|
||||||
|
@ -3,11 +3,12 @@ use crate::editor::document::Document;
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
use lib_ot::core::{
|
use lib_ot::core::{
|
||||||
AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, Path, Selection,
|
AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, NodeTreeContext, Path,
|
||||||
Transaction,
|
Selection, Transaction,
|
||||||
};
|
};
|
||||||
use lib_ot::text_delta::TextOperations;
|
|
||||||
use serde::de::{self, MapAccess, Visitor};
|
use lib_ot::text_delta::DeltaTextOperations;
|
||||||
|
use serde::de::{self, MapAccess, Unexpected, Visitor};
|
||||||
use serde::ser::{SerializeMap, SerializeSeq};
|
use serde::ser::{SerializeMap, SerializeSeq};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -44,14 +45,14 @@ impl<'de> Deserialize<'de> for Document {
|
|||||||
where
|
where
|
||||||
M: MapAccess<'de>,
|
M: MapAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut node_tree = None;
|
let mut document_node = None;
|
||||||
while let Some(key) = map.next_key()? {
|
while let Some(key) = map.next_key()? {
|
||||||
match key {
|
match key {
|
||||||
"document" => {
|
"document" => {
|
||||||
if node_tree.is_some() {
|
if document_node.is_some() {
|
||||||
return Err(de::Error::duplicate_field("document"));
|
return Err(de::Error::duplicate_field("document"));
|
||||||
}
|
}
|
||||||
node_tree = Some(map.next_value::<NodeTree>()?)
|
document_node = Some(map.next_value::<DocumentNode>()?)
|
||||||
}
|
}
|
||||||
s => {
|
s => {
|
||||||
return Err(de::Error::unknown_field(s, FIELDS));
|
return Err(de::Error::unknown_field(s, FIELDS));
|
||||||
@ -59,8 +60,13 @@ impl<'de> Deserialize<'de> for Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match node_tree {
|
match document_node {
|
||||||
Some(tree) => Ok(Document::new(tree)),
|
Some(document_node) => {
|
||||||
|
match NodeTree::from_node_data(document_node.into(), NodeTreeContext::default()) {
|
||||||
|
Ok(tree) => Ok(Document::new(tree)),
|
||||||
|
Err(err) => Err(de::Error::invalid_value(Unexpected::Other(&format!("{}", err)), &"")),
|
||||||
|
}
|
||||||
|
}
|
||||||
None => Err(de::Error::missing_field("document")),
|
None => Err(de::Error::missing_field("document")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,10 +75,20 @@ impl<'de> Deserialize<'de> for Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub fn make_transaction_from_document_content(content: &str) -> FlowyResult<Transaction> {
|
||||||
struct DocumentContentSerializer<'a>(pub &'a Document);
|
let document_node: DocumentNode = serde_json::from_str::<DocumentContentDeserializer>(content)?.document;
|
||||||
|
let document_operation = DocumentOperation::Insert {
|
||||||
|
path: 0_usize.into(),
|
||||||
|
nodes: vec![document_node],
|
||||||
|
};
|
||||||
|
let mut document_transaction = DocumentTransaction::default();
|
||||||
|
document_transaction.operations.push(document_operation);
|
||||||
|
Ok(document_transaction.into())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub struct DocumentContentSerde {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct DocumentTransaction {
|
pub struct DocumentTransaction {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
operations: Vec<DocumentOperation>,
|
operations: Vec<DocumentOperation>,
|
||||||
@ -161,8 +177,8 @@ pub enum DocumentOperation {
|
|||||||
#[serde(rename = "update_text")]
|
#[serde(rename = "update_text")]
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: Path,
|
path: Path,
|
||||||
delta: TextOperations,
|
delta: DeltaTextOperations,
|
||||||
inverted: TextOperations,
|
inverted: DeltaTextOperations,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,20 +246,27 @@ pub struct DocumentNode {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub attributes: AttributeHashMap,
|
pub attributes: AttributeHashMap,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "TextOperations::is_empty")]
|
#[serde(skip_serializing_if = "DeltaTextOperations::is_empty")]
|
||||||
pub delta: TextOperations,
|
#[serde(default)]
|
||||||
|
pub delta: DeltaTextOperations,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub children: Vec<DocumentNode>,
|
pub children: Vec<DocumentNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DocumentNode {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::convert::From<NodeData> for DocumentNode {
|
impl std::convert::From<NodeData> for DocumentNode {
|
||||||
fn from(node_data: NodeData) -> Self {
|
fn from(node_data: NodeData) -> Self {
|
||||||
let delta = if let Body::Delta(operations) = node_data.body {
|
let delta = if let Body::Delta(operations) = node_data.body {
|
||||||
operations
|
operations
|
||||||
} else {
|
} else {
|
||||||
TextOperations::default()
|
DeltaTextOperations::default()
|
||||||
};
|
};
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
node_type: node_data.node_type,
|
node_type: node_data.node_type,
|
||||||
@ -265,6 +288,14 @@ impl std::convert::From<DocumentNode> for NodeData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DocumentContentDeserializer {
|
||||||
|
document: DocumentNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DocumentContentSerializer<'a>(pub &'a Document);
|
||||||
|
|
||||||
impl<'a> Serialize for DocumentContentSerializer<'a> {
|
impl<'a> Serialize for DocumentContentSerializer<'a> {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@ -299,6 +330,12 @@ impl<'a> Serialize for DocumentContentSerializer<'a> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::editor::document::Document;
|
use crate::editor::document::Document;
|
||||||
use crate::editor::document_serde::DocumentTransaction;
|
use crate::editor::document_serde::DocumentTransaction;
|
||||||
|
use crate::editor::initial_read_me;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_read_me() {
|
||||||
|
let _ = initial_read_me();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_deserialize_update_text_operation_test() {
|
fn transaction_deserialize_update_text_operation_test() {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::editor::document::{Document, DocumentRevisionSerde};
|
use crate::editor::document::{Document, DocumentRevisionSerde};
|
||||||
use crate::editor::document_serde::DocumentTransaction;
|
use crate::editor::document_serde::DocumentTransaction;
|
||||||
|
use crate::editor::make_transaction_from_revisions;
|
||||||
use crate::editor::queue::{Command, CommandSender, DocumentQueue};
|
use crate::editor::queue::{Command, CommandSender, DocumentQueue};
|
||||||
use crate::{DocumentEditor, DocumentUser};
|
use crate::{DocumentEditor, DocumentUser};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@ -17,6 +18,7 @@ pub struct AppFlowyDocumentEditor {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
doc_id: String,
|
doc_id: String,
|
||||||
command_sender: CommandSender,
|
command_sender: CommandSender,
|
||||||
|
rev_manager: Arc<RevisionManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppFlowyDocumentEditor {
|
impl AppFlowyDocumentEditor {
|
||||||
@ -28,9 +30,13 @@ impl AppFlowyDocumentEditor {
|
|||||||
) -> FlowyResult<Arc<Self>> {
|
) -> FlowyResult<Arc<Self>> {
|
||||||
let document = rev_manager.load::<DocumentRevisionSerde>(Some(cloud_service)).await?;
|
let document = rev_manager.load::<DocumentRevisionSerde>(Some(cloud_service)).await?;
|
||||||
let rev_manager = Arc::new(rev_manager);
|
let rev_manager = Arc::new(rev_manager);
|
||||||
let command_sender = spawn_edit_queue(user, rev_manager, document);
|
let command_sender = spawn_edit_queue(user, rev_manager.clone(), document);
|
||||||
let doc_id = doc_id.to_string();
|
let doc_id = doc_id.to_string();
|
||||||
let editor = Arc::new(Self { doc_id, command_sender });
|
let editor = Arc::new(Self {
|
||||||
|
doc_id,
|
||||||
|
command_sender,
|
||||||
|
rev_manager,
|
||||||
|
});
|
||||||
Ok(editor)
|
Ok(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +59,13 @@ impl AppFlowyDocumentEditor {
|
|||||||
let content = rx.await.map_err(internal_error)??;
|
let content = rx.await.map_err(internal_error)??;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn duplicate_document(&self) -> FlowyResult<String> {
|
||||||
|
let revisions = self.rev_manager.load_revisions().await?;
|
||||||
|
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||||
|
let json = transaction.to_json()?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_edit_queue(
|
fn spawn_edit_queue(
|
||||||
@ -67,11 +80,24 @@ fn spawn_edit_queue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
|
impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
|
||||||
|
fn close(&self) {}
|
||||||
|
|
||||||
fn export(&self) -> FutureResult<String, FlowyError> {
|
fn export(&self) -> FutureResult<String, FlowyError> {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
FutureResult::new(async move { this.get_content(false).await })
|
FutureResult::new(async move { this.get_content(false).await })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn duplicate(&self) -> FutureResult<String, FlowyError> {
|
||||||
|
let this = self.clone();
|
||||||
|
FutureResult::new(async move { this.duplicate_document().await })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
|
||||||
|
FutureResult::new(async move { Ok(()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_ws_state(&self, _state: &WSConnectState) {}
|
||||||
|
|
||||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
@ -81,14 +107,6 @@ impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self) {}
|
|
||||||
|
|
||||||
fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
|
|
||||||
FutureResult::new(async move { Ok(()) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_ws_state(&self, _state: &WSConnectState) {}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,419 @@
|
|||||||
|
use crate::editor::{DocumentNode, DocumentOperation};
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
|
||||||
|
use lib_ot::core::{AttributeHashMap, DeltaOperation, Insert, Transaction};
|
||||||
|
use lib_ot::text_delta::{DeltaTextOperation, DeltaTextOperations};
|
||||||
|
|
||||||
|
pub struct DeltaRevisionMigration();
|
||||||
|
|
||||||
|
impl DeltaRevisionMigration {
|
||||||
|
pub fn run(delta: DeltaTextOperations) -> FlowyResult<Transaction> {
|
||||||
|
let migrate_background_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||||
|
if let Some(Some(color)) = insert.attributes.get("background").map(|value| value.str_value()) {
|
||||||
|
insert.attributes.remove_key("background");
|
||||||
|
insert.attributes.insert("backgroundColor", color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let migrate_strike_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||||
|
if let Some(Some(_)) = insert.attributes.get("strike").map(|value| value.str_value()) {
|
||||||
|
insert.attributes.remove_key("strike");
|
||||||
|
insert.attributes.insert("strikethrough", true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let migrate_link_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||||
|
if let Some(Some(link)) = insert.attributes.get("link").map(|value| value.str_value()) {
|
||||||
|
insert.attributes.remove_key("link");
|
||||||
|
insert.attributes.insert("href", link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let migrate_list_attribute =
|
||||||
|
|attribute_node: &mut DocumentNode, value: &str, number_list_number: &mut usize| {
|
||||||
|
if value == "unchecked" {
|
||||||
|
*number_list_number = 0;
|
||||||
|
attribute_node.attributes.insert("subtype", "checkbox");
|
||||||
|
attribute_node.attributes.insert("checkbox", false);
|
||||||
|
}
|
||||||
|
if value == "checked" {
|
||||||
|
*number_list_number = 0;
|
||||||
|
attribute_node.attributes.insert("subtype", "checkbox");
|
||||||
|
attribute_node.attributes.insert("checkbox", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "bullet" {
|
||||||
|
*number_list_number = 0;
|
||||||
|
attribute_node.attributes.insert("subtype", "bulleted-list");
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "ordered" {
|
||||||
|
*number_list_number += 1;
|
||||||
|
attribute_node.attributes.insert("subtype", "number-list");
|
||||||
|
attribute_node.attributes.insert("number", *number_list_number);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let generate_new_op_with_double_new_lines = |insert: &mut Insert<AttributeHashMap>| {
|
||||||
|
let pattern = "\n\n";
|
||||||
|
let mut new_ops = vec![];
|
||||||
|
if insert.s.as_str().contains(pattern) {
|
||||||
|
let insert_str = insert.s.clone();
|
||||||
|
let insert_strings = insert_str.split(pattern).map(|s| s.to_owned());
|
||||||
|
for (index, new_s) in insert_strings.enumerate() {
|
||||||
|
if index == 0 {
|
||||||
|
insert.s = new_s.into();
|
||||||
|
} else {
|
||||||
|
new_ops.push(DeltaOperation::Insert(Insert {
|
||||||
|
s: new_s.into(),
|
||||||
|
attributes: AttributeHashMap::default(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_ops
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_text_node = |ops: Vec<DeltaTextOperation>| {
|
||||||
|
let mut document_node = DocumentNode::new();
|
||||||
|
document_node.node_type = "text".to_owned();
|
||||||
|
ops.into_iter().for_each(|op| document_node.delta.add(op));
|
||||||
|
document_node
|
||||||
|
};
|
||||||
|
|
||||||
|
let transform_op = |mut insert: Insert<AttributeHashMap>| {
|
||||||
|
// Rename the attribute name from background to backgroundColor
|
||||||
|
migrate_background_attribute(&mut insert);
|
||||||
|
migrate_strike_attribute(&mut insert);
|
||||||
|
migrate_link_attribute(&mut insert);
|
||||||
|
|
||||||
|
let new_ops = generate_new_op_with_double_new_lines(&mut insert);
|
||||||
|
(DeltaOperation::Insert(insert), new_ops)
|
||||||
|
};
|
||||||
|
let mut index: usize = 0;
|
||||||
|
let mut number_list_number = 0;
|
||||||
|
let mut editor_node = DocumentNode::new();
|
||||||
|
editor_node.node_type = "editor".to_owned();
|
||||||
|
|
||||||
|
let mut transaction = Transaction::new();
|
||||||
|
transaction.push_operation(DocumentOperation::Insert {
|
||||||
|
path: 0.into(),
|
||||||
|
nodes: vec![editor_node],
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut iter = delta.ops.into_iter().enumerate();
|
||||||
|
while let Some((_, op)) = iter.next() {
|
||||||
|
let mut document_node = create_text_node(vec![]);
|
||||||
|
let mut split_document_nodes = vec![];
|
||||||
|
match op {
|
||||||
|
DeltaOperation::Delete(_) => tracing::warn!("Should not contain delete operation"),
|
||||||
|
DeltaOperation::Retain(_) => tracing::warn!("Should not contain retain operation"),
|
||||||
|
DeltaOperation::Insert(insert) => {
|
||||||
|
if insert.s.as_str() != "\n" {
|
||||||
|
let (op, new_ops) = transform_op(insert);
|
||||||
|
document_node.delta.add(op);
|
||||||
|
if !new_ops.is_empty() {
|
||||||
|
split_document_nodes.push(create_text_node(new_ops));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some((_, DeltaOperation::Insert(insert))) = iter.next() {
|
||||||
|
if insert.s.as_str() != "\n" {
|
||||||
|
let (op, new_ops) = transform_op(insert);
|
||||||
|
document_node.delta.add(op);
|
||||||
|
|
||||||
|
if !new_ops.is_empty() {
|
||||||
|
split_document_nodes.push(create_text_node(new_ops));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let attribute_node = match split_document_nodes.last_mut() {
|
||||||
|
None => &mut document_node,
|
||||||
|
Some(split_document_node) => split_document_node,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = insert.attributes.get("header") {
|
||||||
|
attribute_node.attributes.insert("subtype", "heading");
|
||||||
|
if let Some(v) = value.int_value() {
|
||||||
|
number_list_number = 0;
|
||||||
|
attribute_node.attributes.insert("heading", format!("h{}", v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if insert.attributes.get("blockquote").is_some() {
|
||||||
|
attribute_node.attributes.insert("subtype", "quote");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = insert.attributes.get("list") {
|
||||||
|
if let Some(s) = value.str_value() {
|
||||||
|
migrate_list_attribute(attribute_node, &s, &mut number_list_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut operations = vec![document_node];
|
||||||
|
operations.extend(split_document_nodes);
|
||||||
|
operations.into_iter().for_each(|node| {
|
||||||
|
// println!("{}", serde_json::to_string(&node).unwrap());
|
||||||
|
let operation = DocumentOperation::Insert {
|
||||||
|
path: vec![0, index].into(),
|
||||||
|
nodes: vec![node],
|
||||||
|
};
|
||||||
|
transaction.push_operation(operation);
|
||||||
|
index += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::editor::migration::delta_migration::DeltaRevisionMigration;
|
||||||
|
use crate::editor::Document;
|
||||||
|
use lib_ot::text_delta::DeltaTextOperations;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_delta_to_transaction_test() {
|
||||||
|
let delta = DeltaTextOperations::from_json(DELTA_STR).unwrap();
|
||||||
|
let transaction = DeltaRevisionMigration::run(delta).unwrap();
|
||||||
|
let document = Document::from_transaction(transaction).unwrap();
|
||||||
|
let s = document.get_content(true).unwrap();
|
||||||
|
assert!(!s.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
const DELTA_STR: &str = r#"[
|
||||||
|
{
|
||||||
|
"insert": "\n👋 Welcome to AppFlowy!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"header": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\nHere are the basics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"header": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Click anywhere and just start typing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "unchecked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Highlight",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$fff2cd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " any text, and use the menu at the bottom to "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "style",
|
||||||
|
"attributes": {
|
||||||
|
"italic": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "your",
|
||||||
|
"attributes": {
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "writing",
|
||||||
|
"attributes": {
|
||||||
|
"underline": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "however",
|
||||||
|
"attributes": {
|
||||||
|
"code": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "you",
|
||||||
|
"attributes": {
|
||||||
|
"strike": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "like",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$e8e0ff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "unchecked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Click "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "+ New Page",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$defff1",
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " button at the bottom of your sidebar to add a new page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "unchecked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Click the "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "'",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$defff1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "+",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$defff1",
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "'",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$defff1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " next to any page title in the sidebar to quickly add a new subpage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "unchecked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\nHave a question? "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"header": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Click the "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "'?'",
|
||||||
|
"attributes": {
|
||||||
|
"background": "$defff1",
|
||||||
|
"bold": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"header": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "GitHub: https://github.com/AppFlowy-IO/appflowy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"blockquote": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Twitter: https://twitter.com/appflowy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"blockquote": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "Newsletter: https://www.appflowy.io/blog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"blockquote": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "item 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "ordered"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "item 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "ordered"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "item3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "\n",
|
||||||
|
"attributes": {
|
||||||
|
"list": "ordered"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "appflowy",
|
||||||
|
"attributes": {
|
||||||
|
"link": "https://www.appflowy.io/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
mod delta_migration;
|
||||||
|
|
||||||
|
pub use delta_migration::*;
|
@ -2,7 +2,17 @@
|
|||||||
mod document;
|
mod document;
|
||||||
mod document_serde;
|
mod document_serde;
|
||||||
mod editor;
|
mod editor;
|
||||||
|
mod migration;
|
||||||
mod queue;
|
mod queue;
|
||||||
|
|
||||||
pub use document::*;
|
pub use document::*;
|
||||||
|
pub use document_serde::*;
|
||||||
pub use editor::*;
|
pub use editor::*;
|
||||||
|
pub use migration::*;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn initial_read_me() -> String {
|
||||||
|
let document_content = include_str!("READ_ME.json");
|
||||||
|
let transaction = make_transaction_from_document_content(document_content).unwrap();
|
||||||
|
transaction.to_json().unwrap()
|
||||||
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
use crate::editor::document::Document;
|
use crate::editor::document::Document;
|
||||||
use crate::DocumentUser;
|
use crate::DocumentUser;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
|
use bytes::Bytes;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use flowy_revision::RevisionManager;
|
use flowy_revision::RevisionManager;
|
||||||
|
use flowy_sync::entities::revision::{RevId, Revision};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use lib_ot::core::Transaction;
|
use lib_ot::core::Transaction;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::sync::{oneshot, RwLock};
|
use tokio::sync::{oneshot, RwLock};
|
||||||
|
|
||||||
pub struct DocumentQueue {
|
pub struct DocumentQueue {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
user: Arc<dyn DocumentUser>,
|
user: Arc<dyn DocumentUser>,
|
||||||
@ -56,7 +60,10 @@ impl DocumentQueue {
|
|||||||
async fn handle_command(&self, command: Command) -> Result<(), FlowyError> {
|
async fn handle_command(&self, command: Command) -> Result<(), FlowyError> {
|
||||||
match command {
|
match command {
|
||||||
Command::ComposeTransaction { transaction, ret } => {
|
Command::ComposeTransaction { transaction, ret } => {
|
||||||
self.document.write().await.apply_transaction(transaction)?;
|
self.document.write().await.apply_transaction(transaction.clone())?;
|
||||||
|
let _ = self
|
||||||
|
.save_local_operations(transaction, self.document.read().await.md5())
|
||||||
|
.await?;
|
||||||
let _ = ret.send(Ok(()));
|
let _ = ret.send(Ok(()));
|
||||||
}
|
}
|
||||||
Command::GetDocumentContent { pretty, ret } => {
|
Command::GetDocumentContent { pretty, ret } => {
|
||||||
@ -66,6 +73,16 @@ impl DocumentQueue {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self, transaction, md5), err)]
|
||||||
|
async fn save_local_operations(&self, transaction: Transaction, md5: String) -> Result<RevId, FlowyError> {
|
||||||
|
let bytes = Bytes::from(transaction.to_bytes()?);
|
||||||
|
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
||||||
|
let user_id = self.user.user_id()?;
|
||||||
|
let revision = Revision::new(&self.rev_manager.object_id, base_rev_id, rev_id, bytes, &user_id, md5);
|
||||||
|
let _ = self.rev_manager.add_local_revision(&revision).await?;
|
||||||
|
Ok(rev_id.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type CommandSender = Sender<Command>;
|
pub(crate) type CommandSender = Sender<Command>;
|
||||||
|
@ -74,12 +74,41 @@ pub struct ExportPayloadPB {
|
|||||||
|
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub export_type: ExportType,
|
pub export_type: ExportType,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub document_version: DocumentVersionPB,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)]
|
||||||
|
pub enum DocumentVersionPB {
|
||||||
|
/// this version's content of the document is build from `Delta`. It uses
|
||||||
|
/// `DeltaDocumentEditor`.
|
||||||
|
V0 = 0,
|
||||||
|
/// this version's content of the document is build from `NodeTree`. It uses
|
||||||
|
/// `AppFlowyDocumentEditor`
|
||||||
|
V1 = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for DocumentVersionPB {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::V0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf)]
|
||||||
|
pub struct OpenDocumentContextPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub document_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub document_version: DocumentVersionPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ExportParams {
|
pub struct ExportParams {
|
||||||
pub view_id: String,
|
pub view_id: String,
|
||||||
pub export_type: ExportType,
|
pub export_type: ExportType,
|
||||||
|
pub document_version: DocumentVersionPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryInto<ExportParams> for ExportPayloadPB {
|
impl TryInto<ExportParams> for ExportPayloadPB {
|
||||||
@ -88,6 +117,7 @@ impl TryInto<ExportParams> for ExportPayloadPB {
|
|||||||
Ok(ExportParams {
|
Ok(ExportParams {
|
||||||
view_id: self.view_id,
|
view_id: self.view_id,
|
||||||
export_type: self.export_type,
|
export_type: self.export_type,
|
||||||
|
document_version: self.document_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
use crate::entities::{DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB};
|
use crate::entities::{
|
||||||
|
DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, OpenDocumentContextPB,
|
||||||
|
};
|
||||||
use crate::DocumentManager;
|
use crate::DocumentManager;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use flowy_sync::entities::document::DocumentIdPB;
|
|
||||||
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub(crate) async fn get_document_handler(
|
pub(crate) async fn get_document_handler(
|
||||||
data: Data<DocumentIdPB>,
|
data: Data<OpenDocumentContextPB>,
|
||||||
manager: AppData<Arc<DocumentManager>>,
|
manager: AppData<Arc<DocumentManager>>,
|
||||||
) -> DataResult<DocumentSnapshotPB, FlowyError> {
|
) -> DataResult<DocumentSnapshotPB, FlowyError> {
|
||||||
let document_id: DocumentIdPB = data.into_inner();
|
let context: OpenDocumentContextPB = data.into_inner();
|
||||||
let editor = manager.open_document_editor(&document_id).await?;
|
let editor = manager.open_document_editor(&context.document_id).await?;
|
||||||
let operations_str = editor.export().await?;
|
let document_data = editor.export().await?;
|
||||||
data_result(DocumentSnapshotPB {
|
data_result(DocumentSnapshotPB {
|
||||||
doc_id: document_id.into(),
|
doc_id: context.document_id,
|
||||||
snapshot: operations_str,
|
snapshot: document_data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ pub fn create(document_manager: Arc<DocumentManager>) -> Module {
|
|||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||||
#[event_err = "FlowyError"]
|
#[event_err = "FlowyError"]
|
||||||
pub enum DocumentEvent {
|
pub enum DocumentEvent {
|
||||||
#[event(input = "DocumentIdPB", output = "DocumentSnapshotPB")]
|
#[event(input = "OpenDocumentContextPB", output = "DocumentSnapshotPB")]
|
||||||
GetDocument = 0,
|
GetDocument = 0,
|
||||||
|
|
||||||
#[event(input = "EditPayloadPB")]
|
#[event(input = "EditPayloadPB")]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mod entities;
|
pub mod entities;
|
||||||
mod event_handler;
|
mod event_handler;
|
||||||
pub mod event_map;
|
pub mod event_map;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
@ -6,6 +6,7 @@ pub mod manager;
|
|||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod old_editor;
|
pub mod old_editor;
|
||||||
pub mod protobuf;
|
pub mod protobuf;
|
||||||
|
mod services;
|
||||||
|
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub mod errors {
|
pub mod errors {
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
use crate::editor::{initial_document_content, AppFlowyDocumentEditor};
|
use crate::editor::{initial_document_content, AppFlowyDocumentEditor, DocumentRevisionCompress};
|
||||||
use crate::entities::EditParams;
|
use crate::entities::{DocumentVersionPB, EditParams};
|
||||||
use crate::old_editor::editor::{DeltaDocumentEditor, DocumentRevisionCompress};
|
use crate::old_editor::editor::{DeltaDocumentEditor, DeltaDocumentRevisionCompress};
|
||||||
|
use crate::services::DocumentPersistence;
|
||||||
use crate::{errors::FlowyError, DocumentCloudService};
|
use crate::{errors::FlowyError, DocumentCloudService};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use flowy_database::ConnectionPool;
|
use flowy_database::ConnectionPool;
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
use flowy_revision::disk::SQLiteDocumentRevisionPersistence;
|
use flowy_revision::disk::{SQLiteDeltaDocumentRevisionPersistence, SQLiteDocumentRevisionPersistence};
|
||||||
use flowy_revision::{
|
use flowy_revision::{
|
||||||
RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence,
|
RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence,
|
||||||
};
|
};
|
||||||
use flowy_sync::client_document::initial_old_document_content;
|
use flowy_sync::client_document::initial_delta_document_content;
|
||||||
use flowy_sync::entities::{
|
use flowy_sync::entities::{
|
||||||
document::{DocumentIdPB, DocumentOperationsPB},
|
document::DocumentIdPB,
|
||||||
revision::{md5, RepeatedRevision, Revision},
|
revision::{md5, RepeatedRevision, Revision},
|
||||||
ws_data::ServerRevisionWSData,
|
ws_data::ServerRevisionWSData,
|
||||||
};
|
};
|
||||||
use lib_infra::future::FutureResult;
|
use lib_infra::future::FutureResult;
|
||||||
|
|
||||||
use lib_ws::WSConnectState;
|
use lib_ws::WSConnectState;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::{convert::TryInto, sync::Arc};
|
use std::{convert::TryInto, sync::Arc};
|
||||||
@ -26,17 +26,31 @@ pub trait DocumentUser: Send + Sync {
|
|||||||
fn user_dir(&self) -> Result<String, FlowyError>;
|
fn user_dir(&self) -> Result<String, FlowyError>;
|
||||||
fn user_id(&self) -> Result<String, FlowyError>;
|
fn user_id(&self) -> Result<String, FlowyError>;
|
||||||
fn token(&self) -> Result<String, FlowyError>;
|
fn token(&self) -> Result<String, FlowyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DocumentDatabase: Send + Sync {
|
||||||
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait DocumentEditor: Send + Sync {
|
pub trait DocumentEditor: Send + Sync {
|
||||||
fn export(&self) -> FutureResult<String, FlowyError>;
|
/// Called when the document get closed
|
||||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
|
|
||||||
fn close(&self);
|
fn close(&self);
|
||||||
|
|
||||||
|
/// Exports the document content. The content is encoded in the corresponding
|
||||||
|
/// editor data format.
|
||||||
|
fn export(&self) -> FutureResult<String, FlowyError>;
|
||||||
|
|
||||||
|
/// Duplicate the document inner data into String
|
||||||
|
fn duplicate(&self) -> FutureResult<String, FlowyError>;
|
||||||
|
|
||||||
fn receive_ws_data(&self, data: ServerRevisionWSData) -> FutureResult<(), FlowyError>;
|
fn receive_ws_data(&self, data: ServerRevisionWSData) -> FutureResult<(), FlowyError>;
|
||||||
|
|
||||||
fn receive_ws_state(&self, state: &WSConnectState);
|
fn receive_ws_state(&self, state: &WSConnectState);
|
||||||
|
|
||||||
|
/// Receives the local operations made by the user input. The operations are encoded
|
||||||
|
/// in binary format.
|
||||||
|
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
|
||||||
|
|
||||||
/// Returns the `Any` reference that can be used to downcast back to the original,
|
/// Returns the `Any` reference that can be used to downcast back to the original,
|
||||||
/// concrete type.
|
/// concrete type.
|
||||||
///
|
///
|
||||||
@ -50,7 +64,15 @@ pub trait DocumentEditor: Send + Sync {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DocumentConfig {
|
pub struct DocumentConfig {
|
||||||
pub use_new_editor: bool,
|
pub version: DocumentVersionPB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for DocumentConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: DocumentVersionPB::V1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DocumentManager {
|
pub struct DocumentManager {
|
||||||
@ -58,6 +80,8 @@ pub struct DocumentManager {
|
|||||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||||
editor_map: Arc<DocumentEditorMap>,
|
editor_map: Arc<DocumentEditorMap>,
|
||||||
user: Arc<dyn DocumentUser>,
|
user: Arc<dyn DocumentUser>,
|
||||||
|
persistence: Arc<DocumentPersistence>,
|
||||||
|
#[allow(dead_code)]
|
||||||
config: DocumentConfig,
|
config: DocumentConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +89,7 @@ impl DocumentManager {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
cloud_service: Arc<dyn DocumentCloudService>,
|
cloud_service: Arc<dyn DocumentCloudService>,
|
||||||
document_user: Arc<dyn DocumentUser>,
|
document_user: Arc<dyn DocumentUser>,
|
||||||
|
database: Arc<dyn DocumentDatabase>,
|
||||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||||
config: DocumentConfig,
|
config: DocumentConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@ -73,24 +98,31 @@ impl DocumentManager {
|
|||||||
rev_web_socket,
|
rev_web_socket,
|
||||||
editor_map: Arc::new(DocumentEditorMap::new()),
|
editor_map: Arc::new(DocumentEditorMap::new()),
|
||||||
user: document_user,
|
user: document_user,
|
||||||
|
persistence: Arc::new(DocumentPersistence::new(database)),
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) -> FlowyResult<()> {
|
/// Called immediately after the application launched with the user sign in/sign up.
|
||||||
|
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||||
|
pub async fn initialize(&self, user_id: &str) -> FlowyResult<()> {
|
||||||
|
let _ = self.persistence.initialize(user_id)?;
|
||||||
listen_ws_state_changed(self.rev_web_socket.clone(), self.editor_map.clone());
|
listen_ws_state_changed(self.rev_web_socket.clone(), self.editor_map.clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip_all, fields(document_id), err)]
|
||||||
pub async fn open_document_editor<T: AsRef<str>>(
|
pub async fn open_document_editor<T: AsRef<str>>(
|
||||||
&self,
|
&self,
|
||||||
editor_id: T,
|
document_id: T,
|
||||||
) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
||||||
let editor_id = editor_id.as_ref();
|
let document_id = document_id.as_ref();
|
||||||
tracing::Span::current().record("editor_id", &editor_id);
|
tracing::Span::current().record("document_id", &document_id);
|
||||||
self.init_document_editor(editor_id).await
|
self.init_document_editor(document_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||||
@ -101,22 +133,6 @@ impl DocumentManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, payload), err)]
|
|
||||||
pub async fn receive_local_operations(
|
|
||||||
&self,
|
|
||||||
payload: DocumentOperationsPB,
|
|
||||||
) -> Result<DocumentOperationsPB, FlowyError> {
|
|
||||||
let editor = self.get_document_editor(&payload.doc_id).await?;
|
|
||||||
let _ = editor
|
|
||||||
.compose_local_operations(Bytes::from(payload.operations_str))
|
|
||||||
.await?;
|
|
||||||
let operations_str = editor.export().await?;
|
|
||||||
Ok(DocumentOperationsPB {
|
|
||||||
doc_id: payload.doc_id.clone(),
|
|
||||||
operations_str,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
|
pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
|
||||||
let editor = self.get_document_editor(¶ms.doc_id).await?;
|
let editor = self.get_document_editor(¶ms.doc_id).await?;
|
||||||
let _ = editor.compose_local_operations(Bytes::from(params.operations)).await?;
|
let _ = editor.compose_local_operations(Bytes::from(params.operations)).await?;
|
||||||
@ -125,9 +141,9 @@ impl DocumentManager {
|
|||||||
|
|
||||||
pub async fn create_document<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
|
pub async fn create_document<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
|
||||||
let doc_id = doc_id.as_ref().to_owned();
|
let doc_id = doc_id.as_ref().to_owned();
|
||||||
let db_pool = self.user.db_pool()?;
|
let db_pool = self.persistence.database.db_pool()?;
|
||||||
// Maybe we could save the document to disk without creating the RevisionManager
|
// Maybe we could save the document to disk without creating the RevisionManager
|
||||||
let rev_manager = self.make_document_rev_manager(&doc_id, db_pool)?;
|
let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
|
||||||
let _ = rev_manager.reset_object(revisions).await?;
|
let _ = rev_manager.reset_object(revisions).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -149,10 +165,9 @@ impl DocumentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn initial_document_content(&self) -> String {
|
pub fn initial_document_content(&self) -> String {
|
||||||
if self.config.use_new_editor {
|
match self.config.version {
|
||||||
initial_document_content()
|
DocumentVersionPB::V0 => initial_delta_document_content(),
|
||||||
} else {
|
DocumentVersionPB::V1 => initial_document_content(),
|
||||||
initial_old_document_content()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,7 +183,11 @@ impl DocumentManager {
|
|||||||
///
|
///
|
||||||
async fn get_document_editor(&self, doc_id: &str) -> FlowyResult<Arc<dyn DocumentEditor>> {
|
async fn get_document_editor(&self, doc_id: &str) -> FlowyResult<Arc<dyn DocumentEditor>> {
|
||||||
match self.editor_map.get(doc_id) {
|
match self.editor_map.get(doc_id) {
|
||||||
None => self.init_document_editor(doc_id).await,
|
None => {
|
||||||
|
//
|
||||||
|
tracing::warn!("Should call init_document_editor first");
|
||||||
|
self.init_document_editor(doc_id).await
|
||||||
|
}
|
||||||
Some(editor) => Ok(editor),
|
Some(editor) => Ok(editor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,26 +203,40 @@ impl DocumentManager {
|
|||||||
///
|
///
|
||||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||||
pub async fn init_document_editor(&self, doc_id: &str) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
pub async fn init_document_editor(&self, doc_id: &str) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
||||||
let pool = self.user.db_pool()?;
|
let pool = self.persistence.database.db_pool()?;
|
||||||
let user = self.user.clone();
|
let user = self.user.clone();
|
||||||
let token = self.user.token()?;
|
let token = self.user.token()?;
|
||||||
let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
|
|
||||||
let cloud_service = Arc::new(DocumentRevisionCloudService {
|
let cloud_service = Arc::new(DocumentRevisionCloudService {
|
||||||
token,
|
token,
|
||||||
server: self.cloud_service.clone(),
|
server: self.cloud_service.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let editor: Arc<dyn DocumentEditor> = if self.config.use_new_editor {
|
match self.config.version {
|
||||||
let editor = AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?;
|
DocumentVersionPB::V0 => {
|
||||||
Arc::new(editor)
|
let rev_manager = self.make_delta_document_rev_manager(doc_id, pool.clone())?;
|
||||||
} else {
|
let editor: Arc<dyn DocumentEditor> = Arc::new(
|
||||||
let editor =
|
DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service)
|
||||||
DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
|
.await?,
|
||||||
Arc::new(editor)
|
);
|
||||||
};
|
|
||||||
self.editor_map.insert(doc_id, editor.clone());
|
self.editor_map.insert(doc_id, editor.clone());
|
||||||
Ok(editor)
|
Ok(editor)
|
||||||
}
|
}
|
||||||
|
DocumentVersionPB::V1 => {
|
||||||
|
let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
|
||||||
|
let editor: Arc<dyn DocumentEditor> =
|
||||||
|
Arc::new(AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?);
|
||||||
|
self.editor_map.insert(doc_id, editor.clone());
|
||||||
|
Ok(editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
|
||||||
|
match self.config.version {
|
||||||
|
DocumentVersionPB::V0 => self.make_delta_document_rev_manager(doc_id, pool),
|
||||||
|
DocumentVersionPB::V1 => self.make_document_rev_manager(doc_id, pool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn make_document_rev_manager(
|
fn make_document_rev_manager(
|
||||||
&self,
|
&self,
|
||||||
@ -215,13 +248,31 @@ impl DocumentManager {
|
|||||||
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
||||||
// let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
|
// let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
|
||||||
let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
|
let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
|
||||||
let rev_compactor = DocumentRevisionCompress();
|
|
||||||
|
|
||||||
Ok(RevisionManager::new(
|
Ok(RevisionManager::new(
|
||||||
&user_id,
|
&user_id,
|
||||||
doc_id,
|
doc_id,
|
||||||
rev_persistence,
|
rev_persistence,
|
||||||
rev_compactor,
|
DocumentRevisionCompress(),
|
||||||
|
// history_persistence,
|
||||||
|
snapshot_persistence,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_delta_document_rev_manager(
|
||||||
|
&self,
|
||||||
|
doc_id: &str,
|
||||||
|
pool: Arc<ConnectionPool>,
|
||||||
|
) -> Result<RevisionManager, FlowyError> {
|
||||||
|
let user_id = self.user.user_id()?;
|
||||||
|
let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(&user_id, pool.clone());
|
||||||
|
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
||||||
|
// let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
|
||||||
|
let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
|
||||||
|
Ok(RevisionManager::new(
|
||||||
|
&user_id,
|
||||||
|
doc_id,
|
||||||
|
rev_persistence,
|
||||||
|
DeltaDocumentRevisionCompress(),
|
||||||
// history_persistence,
|
// history_persistence,
|
||||||
snapshot_persistence,
|
snapshot_persistence,
|
||||||
))
|
))
|
||||||
|
@ -18,7 +18,7 @@ use lib_infra::future::FutureResult;
|
|||||||
use lib_ot::core::{AttributeEntry, AttributeHashMap};
|
use lib_ot::core::{AttributeEntry, AttributeHashMap};
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::{DeltaOperation, Interval},
|
core::{DeltaOperation, Interval},
|
||||||
text_delta::TextOperations,
|
text_delta::DeltaTextOperations,
|
||||||
};
|
};
|
||||||
use lib_ws::WSConnectState;
|
use lib_ws::WSConnectState;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
@ -46,7 +46,7 @@ impl DeltaDocumentEditor {
|
|||||||
let document = rev_manager
|
let document = rev_manager
|
||||||
.load::<DeltaDocumentRevisionSerde>(Some(cloud_service))
|
.load::<DeltaDocumentRevisionSerde>(Some(cloud_service))
|
||||||
.await?;
|
.await?;
|
||||||
let operations = TextOperations::from_bytes(&document.content)?;
|
let operations = DeltaTextOperations::from_bytes(&document.content)?;
|
||||||
let rev_manager = Arc::new(rev_manager);
|
let rev_manager = Arc::new(rev_manager);
|
||||||
let doc_id = doc_id.to_string();
|
let doc_id = doc_id.to_string();
|
||||||
let user_id = user.user_id()?;
|
let user_id = user.user_id()?;
|
||||||
@ -147,6 +147,11 @@ impl DeltaDocumentEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
||||||
|
fn close(&self) {
|
||||||
|
#[cfg(feature = "sync")]
|
||||||
|
self.ws_manager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
fn export(&self) -> FutureResult<String, FlowyError> {
|
fn export(&self) -> FutureResult<String, FlowyError> {
|
||||||
let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
|
let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
|
||||||
let msg = EditorCommand::GetOperationsString { ret };
|
let msg = EditorCommand::GetOperationsString { ret };
|
||||||
@ -158,22 +163,8 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
fn duplicate(&self) -> FutureResult<String, FlowyError> {
|
||||||
let edit_cmd_tx = self.edit_cmd_tx.clone();
|
self.export()
|
||||||
FutureResult::new(async move {
|
|
||||||
let operations = TextOperations::from_bytes(&data)?;
|
|
||||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
|
||||||
let msg = EditorCommand::ComposeLocalOperations { operations, ret };
|
|
||||||
|
|
||||||
let _ = edit_cmd_tx.send(msg).await;
|
|
||||||
let _ = rx.await.map_err(internal_error)??;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close(&self) {
|
|
||||||
#[cfg(feature = "sync")]
|
|
||||||
self.ws_manager.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@ -193,6 +184,19 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
|||||||
self.ws_manager.connect_state_changed(state.clone());
|
self.ws_manager.connect_state_changed(state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
||||||
|
let edit_cmd_tx = self.edit_cmd_tx.clone();
|
||||||
|
FutureResult::new(async move {
|
||||||
|
let operations = DeltaTextOperations::from_bytes(&data)?;
|
||||||
|
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||||
|
let msg = EditorCommand::ComposeLocalOperations { operations, ret };
|
||||||
|
|
||||||
|
let _ = edit_cmd_tx.send(msg).await;
|
||||||
|
let _ = rx.await.map_err(internal_error)??;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -207,7 +211,7 @@ impl std::ops::Drop for DeltaDocumentEditor {
|
|||||||
fn spawn_edit_queue(
|
fn spawn_edit_queue(
|
||||||
user: Arc<dyn DocumentUser>,
|
user: Arc<dyn DocumentUser>,
|
||||||
rev_manager: Arc<RevisionManager>,
|
rev_manager: Arc<RevisionManager>,
|
||||||
delta: TextOperations,
|
delta: DeltaTextOperations,
|
||||||
) -> EditorCommandSender {
|
) -> EditorCommandSender {
|
||||||
let (sender, receiver) = mpsc::channel(1000);
|
let (sender, receiver) = mpsc::channel(1000);
|
||||||
let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver);
|
let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver);
|
||||||
@ -226,8 +230,8 @@ fn spawn_edit_queue(
|
|||||||
|
|
||||||
#[cfg(feature = "flowy_unit_test")]
|
#[cfg(feature = "flowy_unit_test")]
|
||||||
impl DeltaDocumentEditor {
|
impl DeltaDocumentEditor {
|
||||||
pub async fn document_operations(&self) -> FlowyResult<TextOperations> {
|
pub async fn document_operations(&self) -> FlowyResult<DeltaTextOperations> {
|
||||||
let (ret, rx) = oneshot::channel::<CollaborateResult<TextOperations>>();
|
let (ret, rx) = oneshot::channel::<CollaborateResult<DeltaTextOperations>>();
|
||||||
let msg = EditorCommand::GetOperations { ret };
|
let msg = EditorCommand::GetOperations { ret };
|
||||||
let _ = self.edit_cmd_tx.send(msg).await;
|
let _ = self.edit_cmd_tx.send(msg).await;
|
||||||
let delta = rx.await.map_err(internal_error)??;
|
let delta = rx.await.map_err(internal_error)??;
|
||||||
@ -264,8 +268,8 @@ impl RevisionObjectSerializer for DeltaDocumentRevisionSerde {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct DocumentRevisionCompress();
|
pub(crate) struct DeltaDocumentRevisionCompress();
|
||||||
impl RevisionCompress for DocumentRevisionCompress {
|
impl RevisionCompress for DeltaDocumentRevisionCompress {
|
||||||
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||||
DeltaDocumentRevisionSerde::combine_revisions(revisions)
|
DeltaDocumentRevisionSerde::combine_revisions(revisions)
|
||||||
}
|
}
|
||||||
@ -273,7 +277,7 @@ impl RevisionCompress for DocumentRevisionCompress {
|
|||||||
|
|
||||||
// quill-editor requires the delta should end with '\n' and only contains the
|
// quill-editor requires the delta should end with '\n' and only contains the
|
||||||
// insert operation. The function, correct_delta maybe be removed in the future.
|
// insert operation. The function, correct_delta maybe be removed in the future.
|
||||||
fn correct_delta(delta: &mut TextOperations) {
|
fn correct_delta(delta: &mut DeltaTextOperations) {
|
||||||
if let Some(op) = delta.ops.last() {
|
if let Some(op) = delta.ops.last() {
|
||||||
let op_data = op.get_data();
|
let op_data = op.get_data();
|
||||||
if !op_data.ends_with('\n') {
|
if !op_data.ends_with('\n') {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::old_editor::web_socket::DocumentResolveOperations;
|
use crate::old_editor::web_socket::DeltaDocumentResolveOperations;
|
||||||
use crate::DocumentUser;
|
use crate::DocumentUser;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
@ -12,7 +12,7 @@ use futures::stream::StreamExt;
|
|||||||
use lib_ot::core::AttributeEntry;
|
use lib_ot::core::AttributeEntry;
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::{Interval, OperationTransform},
|
core::{Interval, OperationTransform},
|
||||||
text_delta::TextOperations,
|
text_delta::DeltaTextOperations,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
@ -31,7 +31,7 @@ impl EditDocumentQueue {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
user: Arc<dyn DocumentUser>,
|
user: Arc<dyn DocumentUser>,
|
||||||
rev_manager: Arc<RevisionManager>,
|
rev_manager: Arc<RevisionManager>,
|
||||||
operations: TextOperations,
|
operations: DeltaTextOperations,
|
||||||
receiver: EditorCommandReceiver,
|
receiver: EditorCommandReceiver,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations)));
|
let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations)));
|
||||||
@ -91,8 +91,8 @@ impl EditDocumentQueue {
|
|||||||
EditorCommand::TransformOperations { operations, ret } => {
|
EditorCommand::TransformOperations { operations, ret } => {
|
||||||
let f = || async {
|
let f = || async {
|
||||||
let read_guard = self.document.read().await;
|
let read_guard = self.document.read().await;
|
||||||
let mut server_operations: Option<DocumentResolveOperations> = None;
|
let mut server_operations: Option<DeltaDocumentResolveOperations> = None;
|
||||||
let client_operations: TextOperations;
|
let client_operations: DeltaTextOperations;
|
||||||
|
|
||||||
if read_guard.is_empty() {
|
if read_guard.is_empty() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@ -100,11 +100,11 @@ impl EditDocumentQueue {
|
|||||||
} else {
|
} else {
|
||||||
let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?;
|
let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?;
|
||||||
client_operations = c_prime;
|
client_operations = c_prime;
|
||||||
server_operations = Some(DocumentResolveOperations(s_prime));
|
server_operations = Some(DeltaDocumentResolveOperations(s_prime));
|
||||||
}
|
}
|
||||||
drop(read_guard);
|
drop(read_guard);
|
||||||
Ok::<TextTransformOperations, CollaborateError>(TransformOperations {
|
Ok::<TextTransformOperations, CollaborateError>(TransformOperations {
|
||||||
client_operations: DocumentResolveOperations(client_operations),
|
client_operations: DeltaDocumentResolveOperations(client_operations),
|
||||||
server_operations,
|
server_operations,
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@ -174,7 +174,7 @@ impl EditDocumentQueue {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_local_operations(&self, operations: TextOperations, md5: String) -> Result<RevId, FlowyError> {
|
async fn save_local_operations(&self, operations: DeltaTextOperations, md5: String) -> Result<RevId, FlowyError> {
|
||||||
let bytes = operations.json_bytes();
|
let bytes = operations.json_bytes();
|
||||||
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
||||||
let user_id = self.user.user_id()?;
|
let user_id = self.user.user_id()?;
|
||||||
@ -184,26 +184,26 @@ impl EditDocumentQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type TextTransformOperations = TransformOperations<DocumentResolveOperations>;
|
pub type TextTransformOperations = TransformOperations<DeltaDocumentResolveOperations>;
|
||||||
pub(crate) type EditorCommandSender = Sender<EditorCommand>;
|
pub(crate) type EditorCommandSender = Sender<EditorCommand>;
|
||||||
pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
|
pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
|
||||||
pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
|
pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
|
||||||
|
|
||||||
pub(crate) enum EditorCommand {
|
pub(crate) enum EditorCommand {
|
||||||
ComposeLocalOperations {
|
ComposeLocalOperations {
|
||||||
operations: TextOperations,
|
operations: DeltaTextOperations,
|
||||||
ret: Ret<()>,
|
ret: Ret<()>,
|
||||||
},
|
},
|
||||||
ComposeRemoteOperation {
|
ComposeRemoteOperation {
|
||||||
client_operations: TextOperations,
|
client_operations: DeltaTextOperations,
|
||||||
ret: Ret<OperationsMD5>,
|
ret: Ret<OperationsMD5>,
|
||||||
},
|
},
|
||||||
ResetOperations {
|
ResetOperations {
|
||||||
operations: TextOperations,
|
operations: DeltaTextOperations,
|
||||||
ret: Ret<OperationsMD5>,
|
ret: Ret<OperationsMD5>,
|
||||||
},
|
},
|
||||||
TransformOperations {
|
TransformOperations {
|
||||||
operations: TextOperations,
|
operations: DeltaTextOperations,
|
||||||
ret: Ret<TextTransformOperations>,
|
ret: Ret<TextTransformOperations>,
|
||||||
},
|
},
|
||||||
Insert {
|
Insert {
|
||||||
@ -242,7 +242,7 @@ pub(crate) enum EditorCommand {
|
|||||||
},
|
},
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
GetOperations {
|
GetOperations {
|
||||||
ret: Ret<TextOperations>,
|
ret: Ret<DeltaTextOperations>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,33 +13,35 @@ use flowy_sync::{
|
|||||||
errors::CollaborateResult,
|
errors::CollaborateResult,
|
||||||
};
|
};
|
||||||
use lib_infra::future::{BoxResultFuture, FutureResult};
|
use lib_infra::future::{BoxResultFuture, FutureResult};
|
||||||
use lib_ot::text_delta::TextOperations;
|
use lib_ot::text_delta::DeltaTextOperations;
|
||||||
use lib_ws::WSConnectState;
|
use lib_ws::WSConnectState;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use tokio::sync::{broadcast, oneshot};
|
use tokio::sync::{broadcast, oneshot};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DocumentResolveOperations(pub TextOperations);
|
pub struct DeltaDocumentResolveOperations(pub DeltaTextOperations);
|
||||||
|
|
||||||
impl OperationsDeserializer<DocumentResolveOperations> for DocumentResolveOperations {
|
impl OperationsDeserializer<DeltaDocumentResolveOperations> for DeltaDocumentResolveOperations {
|
||||||
fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DocumentResolveOperations> {
|
fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DeltaDocumentResolveOperations> {
|
||||||
Ok(DocumentResolveOperations(make_operations_from_revisions(revisions)?))
|
Ok(DeltaDocumentResolveOperations(make_operations_from_revisions(
|
||||||
|
revisions,
|
||||||
|
)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OperationsSerializer for DocumentResolveOperations {
|
impl OperationsSerializer for DeltaDocumentResolveOperations {
|
||||||
fn serialize_operations(&self) -> Bytes {
|
fn serialize_operations(&self) -> Bytes {
|
||||||
self.0.json_bytes()
|
self.0.json_bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentResolveOperations {
|
impl DeltaDocumentResolveOperations {
|
||||||
pub fn into_inner(self) -> TextOperations {
|
pub fn into_inner(self) -> DeltaTextOperations {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DocumentConflictController = ConflictController<DocumentResolveOperations>;
|
pub type DocumentConflictController = ConflictController<DeltaDocumentResolveOperations>;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) async fn make_document_ws_manager(
|
pub(crate) async fn make_document_ws_manager(
|
||||||
@ -129,8 +131,11 @@ struct DocumentConflictResolver {
|
|||||||
edit_cmd_tx: EditorCommandSender,
|
edit_cmd_tx: EditorCommandSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
impl ConflictResolver<DeltaDocumentResolveOperations> for DocumentConflictResolver {
|
||||||
fn compose_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
fn compose_operations(
|
||||||
|
&self,
|
||||||
|
operations: DeltaDocumentResolveOperations,
|
||||||
|
) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||||
let tx = self.edit_cmd_tx.clone();
|
let tx = self.edit_cmd_tx.clone();
|
||||||
let operations = operations.into_inner();
|
let operations = operations.into_inner();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@ -150,8 +155,8 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
|||||||
|
|
||||||
fn transform_operations(
|
fn transform_operations(
|
||||||
&self,
|
&self,
|
||||||
operations: DocumentResolveOperations,
|
operations: DeltaDocumentResolveOperations,
|
||||||
) -> BoxResultFuture<TransformOperations<DocumentResolveOperations>, FlowyError> {
|
) -> BoxResultFuture<TransformOperations<DeltaDocumentResolveOperations>, FlowyError> {
|
||||||
let tx = self.edit_cmd_tx.clone();
|
let tx = self.edit_cmd_tx.clone();
|
||||||
let operations = operations.into_inner();
|
let operations = operations.into_inner();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@ -166,7 +171,10 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
fn reset_operations(
|
||||||
|
&self,
|
||||||
|
operations: DeltaDocumentResolveOperations,
|
||||||
|
) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||||
let tx = self.edit_cmd_tx.clone();
|
let tx = self.edit_cmd_tx.clone();
|
||||||
let operations = operations.into_inner();
|
let operations = operations.into_inner();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
75
frontend/rust-lib/flowy-document/src/services/migration.rs
Normal file
75
frontend/rust-lib/flowy-document/src/services/migration.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use crate::editor::DeltaRevisionMigration;
|
||||||
|
use crate::DocumentDatabase;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use flowy_database::kv::KV;
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
use flowy_revision::disk::{DeltaRevisionSql, RevisionDiskCache, RevisionRecord, SQLiteDocumentRevisionPersistence};
|
||||||
|
use flowy_sync::entities::revision::{md5, Revision};
|
||||||
|
use flowy_sync::util::make_operations_from_revisions;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const V1_MIGRATION: &str = "DOCUMENT_V1_MIGRATION";
|
||||||
|
pub(crate) struct DocumentMigration {
|
||||||
|
user_id: String,
|
||||||
|
database: Arc<dyn DocumentDatabase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentMigration {
|
||||||
|
pub fn new(user_id: &str, database: Arc<dyn DocumentDatabase>) -> Self {
|
||||||
|
let user_id = user_id.to_owned();
|
||||||
|
Self { user_id, database }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_v1_migration(&self) -> FlowyResult<()> {
|
||||||
|
let key = migration_flag_key(&self.user_id, V1_MIGRATION);
|
||||||
|
if KV::get_bool(&key) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = self.database.db_pool()?;
|
||||||
|
let conn = &*pool.get()?;
|
||||||
|
let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool);
|
||||||
|
let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?;
|
||||||
|
tracing::info!("[Document Migration]: try migrate {} documents", documents.len());
|
||||||
|
for revisions in documents {
|
||||||
|
if revisions.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let document_id = revisions.first().unwrap().object_id.clone();
|
||||||
|
match make_operations_from_revisions(revisions) {
|
||||||
|
Ok(delta) => match DeltaRevisionMigration::run(delta) {
|
||||||
|
Ok(transaction) => {
|
||||||
|
let bytes = Bytes::from(transaction.to_bytes()?);
|
||||||
|
let md5 = format!("{:x}", md5::compute(&bytes));
|
||||||
|
let revision = Revision::new(&document_id, 0, 1, bytes, &self.user_id, md5);
|
||||||
|
let record = RevisionRecord::new(revision);
|
||||||
|
match disk_cache.create_revision_records(vec![record]) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("[Document Migration]: Save revisions to disk failed {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
"[Document Migration]: Migrate revisions to transaction failed {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
KV::set_bool(&key, true);
|
||||||
|
tracing::info!("Run document v1 migration");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn migration_flag_key(user_id: &str, version: &str) -> String {
|
||||||
|
md5(format!("{}{}", user_id, version,))
|
||||||
|
}
|
4
frontend/rust-lib/flowy-document/src/services/mod.rs
Normal file
4
frontend/rust-lib/flowy-document/src/services/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod migration;
|
||||||
|
mod persistence;
|
||||||
|
|
||||||
|
pub use persistence::*;
|
23
frontend/rust-lib/flowy-document/src/services/persistence.rs
Normal file
23
frontend/rust-lib/flowy-document/src/services/persistence.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use crate::services::migration::DocumentMigration;
|
||||||
|
use crate::DocumentDatabase;
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct DocumentPersistence {
|
||||||
|
pub database: Arc<dyn DocumentDatabase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentPersistence {
|
||||||
|
pub fn new(database: Arc<dyn DocumentDatabase>) -> Self {
|
||||||
|
Self { database }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||||
|
pub fn initialize(&self, user_id: &str) -> FlowyResult<()> {
|
||||||
|
let migration = DocumentMigration::new(user_id, self.database.clone());
|
||||||
|
if let Err(e) = migration.run_v1_migration() {
|
||||||
|
tracing::error!("[Document Migration]: run v1 migration failed: {:?}", e);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ use crate::editor::{TestBuilder, TestOp::*};
|
|||||||
use flowy_sync::client_document::{NewlineDocument, EmptyDocument};
|
use flowy_sync::client_document::{NewlineDocument, EmptyDocument};
|
||||||
use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString};
|
use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use lib_ot::text_delta::TextOperations;
|
use lib_ot::text_delta::DeltaTextOperations;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn attributes_bold_added() {
|
fn attributes_bold_added() {
|
||||||
@ -29,7 +29,7 @@ fn attributes_bold_added_and_invert_all() {
|
|||||||
Bold(0, Interval::new(0, 3), true),
|
Bold(0, Interval::new(0, 3), true),
|
||||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
||||||
Bold(0, Interval::new(0, 3), false),
|
Bold(0, Interval::new(0, 3), false),
|
||||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":false}}]"#),
|
||||||
];
|
];
|
||||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ fn attributes_bold_added_and_invert_partial_suffix() {
|
|||||||
Bold(0, Interval::new(0, 4), true),
|
Bold(0, Interval::new(0, 4), true),
|
||||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||||
Bold(0, Interval::new(2, 4), false),
|
Bold(0, Interval::new(2, 4), false),
|
||||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
|
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
|
||||||
];
|
];
|
||||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ fn attributes_bold_added_and_invert_partial_suffix2() {
|
|||||||
Bold(0, Interval::new(0, 4), true),
|
Bold(0, Interval::new(0, 4), true),
|
||||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||||
Bold(0, Interval::new(2, 4), false),
|
Bold(0, Interval::new(2, 4), false),
|
||||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
|
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
|
||||||
Bold(0, Interval::new(2, 4), true),
|
Bold(0, Interval::new(2, 4), true),
|
||||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||||
];
|
];
|
||||||
@ -95,7 +95,7 @@ fn attributes_bold_added_and_invert_partial_prefix() {
|
|||||||
Bold(0, Interval::new(0, 4), true),
|
Bold(0, Interval::new(0, 4), true),
|
||||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||||
Bold(0, Interval::new(0, 2), false),
|
Bold(0, Interval::new(0, 2), false),
|
||||||
AssertDocJson(0, r#"[{"insert":"12"},{"insert":"34","attributes":{"bold":true}}]"#),
|
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":false}},{"insert":"34","attributes":{"bold":true}}]"#),
|
||||||
];
|
];
|
||||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||||
}
|
}
|
||||||
@ -762,12 +762,12 @@ fn attributes_preserve_list_format_on_merge() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_compose() {
|
fn delta_compose() {
|
||||||
let mut delta = TextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
|
let mut delta = DeltaTextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
|
||||||
let deltas = vec![
|
let deltas = vec![
|
||||||
TextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
|
DeltaTextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||||
TextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
|
DeltaTextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
|
||||||
TextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
|
DeltaTextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||||
TextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
|
DeltaTextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for d in deltas {
|
for d in deltas {
|
||||||
|
@ -8,7 +8,7 @@ use derive_more::Display;
|
|||||||
use flowy_sync::client_document::{ClientDocument, InitialDocument};
|
use flowy_sync::client_document::{ClientDocument, InitialDocument};
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::*,
|
core::*,
|
||||||
text_delta::{BuildInTextAttribute, TextOperations},
|
text_delta::{BuildInTextAttribute, DeltaTextOperations},
|
||||||
};
|
};
|
||||||
use rand::{prelude::*, Rng as WrappedRng};
|
use rand::{prelude::*, Rng as WrappedRng};
|
||||||
use std::{sync::Once, time::Duration};
|
use std::{sync::Once, time::Duration};
|
||||||
@ -81,8 +81,8 @@ pub enum TestOp {
|
|||||||
|
|
||||||
pub struct TestBuilder {
|
pub struct TestBuilder {
|
||||||
documents: Vec<ClientDocument>,
|
documents: Vec<ClientDocument>,
|
||||||
deltas: Vec<Option<TextOperations>>,
|
deltas: Vec<Option<DeltaTextOperations>>,
|
||||||
primes: Vec<Option<TextOperations>>,
|
primes: Vec<Option<DeltaTextOperations>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestBuilder {
|
impl TestBuilder {
|
||||||
@ -226,20 +226,20 @@ impl TestBuilder {
|
|||||||
|
|
||||||
TestOp::AssertDocJson(delta_i, expected) => {
|
TestOp::AssertDocJson(delta_i, expected) => {
|
||||||
let delta_json = self.documents[*delta_i].get_operations_json();
|
let delta_json = self.documents[*delta_i].get_operations_json();
|
||||||
let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
|
let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||||
let target_delta: TextOperations = serde_json::from_str(&delta_json).unwrap();
|
let target_delta: DeltaTextOperations = serde_json::from_str(&delta_json).unwrap();
|
||||||
|
|
||||||
if expected_delta != target_delta {
|
if expected_delta != target_delta {
|
||||||
log::error!("✅ expect: {}", expected,);
|
println!("✅ expect: {}", expected,);
|
||||||
log::error!("❌ receive: {}", delta_json);
|
println!("❌ receive: {}", delta_json);
|
||||||
}
|
}
|
||||||
assert_eq!(target_delta, expected_delta);
|
assert_eq!(target_delta, expected_delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
TestOp::AssertPrimeJson(doc_i, expected) => {
|
TestOp::AssertPrimeJson(doc_i, expected) => {
|
||||||
let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str();
|
let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str();
|
||||||
let expected_prime: TextOperations = serde_json::from_str(expected).unwrap();
|
let expected_prime: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||||
let target_prime: TextOperations = serde_json::from_str(&prime_json).unwrap();
|
let target_prime: DeltaTextOperations = serde_json::from_str(&prime_json).unwrap();
|
||||||
|
|
||||||
if expected_prime != target_prime {
|
if expected_prime != target_prime {
|
||||||
log::error!("✅ expect prime: {}", expected,);
|
log::error!("✅ expect prime: {}", expected,);
|
||||||
@ -297,8 +297,8 @@ impl Rng {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen_delta(&mut self, s: &str) -> TextOperations {
|
pub fn gen_delta(&mut self, s: &str) -> DeltaTextOperations {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let s = OTString::from(s);
|
let s = OTString::from(s);
|
||||||
loop {
|
loop {
|
||||||
let left = s.utf16_len() - delta.utf16_base_len;
|
let left = s.utf16_len() - delta.utf16_base_len;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
||||||
use flowy_sync::client_document::{EmptyDocument, NewlineDocument};
|
use flowy_sync::client_document::{EmptyDocument, NewlineDocument};
|
||||||
use lib_ot::text_delta::TextOperationBuilder;
|
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||||
use lib_ot::{core::Interval, core::*, text_delta::TextOperations};
|
use lib_ot::{core::Interval, core::*, text_delta::DeltaTextOperations};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn attributes_insert_text() {
|
fn attributes_insert_text() {
|
||||||
@ -37,7 +37,7 @@ fn attributes_insert_text_at_middle() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_1() {
|
fn delta_get_ops_in_interval_1() {
|
||||||
let operations = OperationsBuilder::new().insert("123").insert("4").build();
|
let operations = OperationsBuilder::new().insert("123").insert("4").build();
|
||||||
let delta = TextOperationBuilder::from_operations(operations);
|
let delta = DeltaTextOperationBuilder::from_operations(operations);
|
||||||
|
|
||||||
let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4));
|
let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4));
|
||||||
assert_eq!(iterator.ops(), delta.ops);
|
assert_eq!(iterator.ops(), delta.ops);
|
||||||
@ -45,7 +45,7 @@ fn delta_get_ops_in_interval_1() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_2() {
|
fn delta_get_ops_in_interval_2() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("123");
|
let insert_a = DeltaOperation::insert("123");
|
||||||
let insert_b = DeltaOperation::insert("4");
|
let insert_b = DeltaOperation::insert("4");
|
||||||
let insert_c = DeltaOperation::insert("5");
|
let insert_c = DeltaOperation::insert("5");
|
||||||
@ -89,7 +89,7 @@ fn delta_get_ops_in_interval_2() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_3() {
|
fn delta_get_ops_in_interval_3() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("123456");
|
let insert_a = DeltaOperation::insert("123456");
|
||||||
delta.add(insert_a.clone());
|
delta.add(insert_a.clone());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -100,7 +100,7 @@ fn delta_get_ops_in_interval_3() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_4() {
|
fn delta_get_ops_in_interval_4() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("12");
|
let insert_a = DeltaOperation::insert("12");
|
||||||
let insert_b = DeltaOperation::insert("34");
|
let insert_b = DeltaOperation::insert("34");
|
||||||
let insert_c = DeltaOperation::insert("56");
|
let insert_c = DeltaOperation::insert("56");
|
||||||
@ -130,7 +130,7 @@ fn delta_get_ops_in_interval_4() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_5() {
|
fn delta_get_ops_in_interval_5() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("123456");
|
let insert_a = DeltaOperation::insert("123456");
|
||||||
let insert_b = DeltaOperation::insert("789");
|
let insert_b = DeltaOperation::insert("789");
|
||||||
delta.ops.push(insert_a.clone());
|
delta.ops.push(insert_a.clone());
|
||||||
@ -148,7 +148,7 @@ fn delta_get_ops_in_interval_5() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_6() {
|
fn delta_get_ops_in_interval_6() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("12345678");
|
let insert_a = DeltaOperation::insert("12345678");
|
||||||
delta.add(insert_a.clone());
|
delta.add(insert_a.clone());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -159,7 +159,7 @@ fn delta_get_ops_in_interval_6() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_get_ops_in_interval_7() {
|
fn delta_get_ops_in_interval_7() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("12345");
|
let insert_a = DeltaOperation::insert("12345");
|
||||||
let retain_a = DeltaOperation::retain(3);
|
let retain_a = DeltaOperation::retain(3);
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ fn delta_get_ops_in_interval_7() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_op_seek() {
|
fn delta_op_seek() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let insert_a = DeltaOperation::insert("12345");
|
let insert_a = DeltaOperation::insert("12345");
|
||||||
let retain_a = DeltaOperation::retain(3);
|
let retain_a = DeltaOperation::retain(3);
|
||||||
delta.add(insert_a.clone());
|
delta.add(insert_a.clone());
|
||||||
@ -191,7 +191,7 @@ fn delta_op_seek() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_utf16_code_unit_seek() {
|
fn delta_utf16_code_unit_seek() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
|
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
@ -201,7 +201,7 @@ fn delta_utf16_code_unit_seek() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_utf16_code_unit_seek_with_attributes() {
|
fn delta_utf16_code_unit_seek_with_attributes() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
let attributes = AttributeBuilder::new()
|
let attributes = AttributeBuilder::new()
|
||||||
.insert("bold", true)
|
.insert("bold", true)
|
||||||
.insert("italic", true)
|
.insert("italic", true)
|
||||||
@ -221,7 +221,7 @@ fn delta_utf16_code_unit_seek_with_attributes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_len() {
|
fn delta_next_op_len() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
|
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
|
||||||
@ -232,7 +232,7 @@ fn delta_next_op_len() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_len_with_chinese() {
|
fn delta_next_op_len_with_chinese() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("你好"));
|
delta.add(DeltaOperation::insert("你好"));
|
||||||
|
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
@ -242,7 +242,7 @@ fn delta_next_op_len_with_chinese() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_len_with_english() {
|
fn delta_next_op_len_with_english() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("ab"));
|
delta.add(DeltaOperation::insert("ab"));
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
assert_eq!(iter.next_op_len().unwrap(), 2);
|
assert_eq!(iter.next_op_len().unwrap(), 2);
|
||||||
@ -251,7 +251,7 @@ fn delta_next_op_len_with_english() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_len_after_seek() {
|
fn delta_next_op_len_after_seek() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
assert_eq!(iter.next_op_len().unwrap(), 5);
|
assert_eq!(iter.next_op_len().unwrap(), 5);
|
||||||
@ -264,7 +264,7 @@ fn delta_next_op_len_after_seek() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_len_none() {
|
fn delta_next_op_len_none() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ fn delta_next_op_len_none() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_with_len_zero() {
|
fn delta_next_op_with_len_zero() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
let mut iter = OperationIterator::new(&delta);
|
let mut iter = OperationIterator::new(&delta);
|
||||||
assert_eq!(iter.next_op_with_len(0), None,);
|
assert_eq!(iter.next_op_with_len(0), None,);
|
||||||
@ -284,7 +284,7 @@ fn delta_next_op_with_len_zero() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_next_op_with_len_cross_op_return_last() {
|
fn delta_next_op_with_len_cross_op_return_last() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("12345"));
|
delta.add(DeltaOperation::insert("12345"));
|
||||||
delta.add(DeltaOperation::retain(1));
|
delta.add(DeltaOperation::retain(1));
|
||||||
delta.add(DeltaOperation::insert("678"));
|
delta.add(DeltaOperation::insert("678"));
|
||||||
@ -297,7 +297,7 @@ fn delta_next_op_with_len_cross_op_return_last() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lengths() {
|
fn lengths() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
assert_eq!(delta.utf16_base_len, 0);
|
assert_eq!(delta.utf16_base_len, 0);
|
||||||
assert_eq!(delta.utf16_target_len, 0);
|
assert_eq!(delta.utf16_target_len, 0);
|
||||||
delta.retain(5, AttributeHashMap::default());
|
delta.retain(5, AttributeHashMap::default());
|
||||||
@ -315,7 +315,7 @@ fn lengths() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn sequence() {
|
fn sequence() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.retain(5, AttributeHashMap::default());
|
delta.retain(5, AttributeHashMap::default());
|
||||||
delta.retain(0, AttributeHashMap::default());
|
delta.retain(0, AttributeHashMap::default());
|
||||||
delta.insert("appflowy", AttributeHashMap::default());
|
delta.insert("appflowy", AttributeHashMap::default());
|
||||||
@ -348,7 +348,7 @@ fn apply_test() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn base_len_test() {
|
fn base_len_test() {
|
||||||
let mut delta_a = TextOperations::default();
|
let mut delta_a = DeltaTextOperations::default();
|
||||||
delta_a.insert("a", AttributeHashMap::default());
|
delta_a.insert("a", AttributeHashMap::default());
|
||||||
delta_a.insert("b", AttributeHashMap::default());
|
delta_a.insert("b", AttributeHashMap::default());
|
||||||
delta_a.insert("c", AttributeHashMap::default());
|
delta_a.insert("c", AttributeHashMap::default());
|
||||||
@ -387,7 +387,7 @@ fn invert_test() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_ops() {
|
fn empty_ops() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.retain(0, AttributeHashMap::default());
|
delta.retain(0, AttributeHashMap::default());
|
||||||
delta.insert("", AttributeHashMap::default());
|
delta.insert("", AttributeHashMap::default());
|
||||||
delta.delete(0);
|
delta.delete(0);
|
||||||
@ -395,12 +395,12 @@ fn empty_ops() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn eq() {
|
fn eq() {
|
||||||
let mut delta_a = TextOperations::default();
|
let mut delta_a = DeltaTextOperations::default();
|
||||||
delta_a.delete(1);
|
delta_a.delete(1);
|
||||||
delta_a.insert("lo", AttributeHashMap::default());
|
delta_a.insert("lo", AttributeHashMap::default());
|
||||||
delta_a.retain(2, AttributeHashMap::default());
|
delta_a.retain(2, AttributeHashMap::default());
|
||||||
delta_a.retain(3, AttributeHashMap::default());
|
delta_a.retain(3, AttributeHashMap::default());
|
||||||
let mut delta_b = TextOperations::default();
|
let mut delta_b = DeltaTextOperations::default();
|
||||||
delta_b.delete(1);
|
delta_b.delete(1);
|
||||||
delta_b.insert("l", AttributeHashMap::default());
|
delta_b.insert("l", AttributeHashMap::default());
|
||||||
delta_b.insert("o", AttributeHashMap::default());
|
delta_b.insert("o", AttributeHashMap::default());
|
||||||
@ -412,7 +412,7 @@ fn eq() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn ops_merging() {
|
fn ops_merging() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
assert_eq!(delta.ops.len(), 0);
|
assert_eq!(delta.ops.len(), 0);
|
||||||
delta.retain(2, AttributeHashMap::default());
|
delta.retain(2, AttributeHashMap::default());
|
||||||
assert_eq!(delta.ops.len(), 1);
|
assert_eq!(delta.ops.len(), 1);
|
||||||
@ -436,7 +436,7 @@ fn ops_merging() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_noop() {
|
fn is_noop() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
assert!(delta.is_noop());
|
assert!(delta.is_noop());
|
||||||
delta.retain(5, AttributeHashMap::default());
|
delta.retain(5, AttributeHashMap::default());
|
||||||
assert!(delta.is_noop());
|
assert!(delta.is_noop());
|
||||||
@ -484,13 +484,13 @@ fn transform_random_delta() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transform_with_two_delta() {
|
fn transform_with_two_delta() {
|
||||||
let mut a = TextOperations::default();
|
let mut a = DeltaTextOperations::default();
|
||||||
let mut a_s = String::new();
|
let mut a_s = String::new();
|
||||||
a.insert("123", AttributeBuilder::new().insert("bold", true).build());
|
a.insert("123", AttributeBuilder::new().insert("bold", true).build());
|
||||||
a_s = a.apply(&a_s).unwrap();
|
a_s = a.apply(&a_s).unwrap();
|
||||||
assert_eq!(&a_s, "123");
|
assert_eq!(&a_s, "123");
|
||||||
|
|
||||||
let mut b = TextOperations::default();
|
let mut b = DeltaTextOperations::default();
|
||||||
let mut b_s = String::new();
|
let mut b_s = String::new();
|
||||||
b.insert("456", AttributeHashMap::default());
|
b.insert("456", AttributeHashMap::default());
|
||||||
b_s = b.apply(&b_s).unwrap();
|
b_s = b.apply(&b_s).unwrap();
|
||||||
@ -580,10 +580,10 @@ fn transform_two_conflict_non_seq_delta() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_invert_no_attribute_delta() {
|
fn delta_invert_no_attribute_delta() {
|
||||||
let mut delta = TextOperations::default();
|
let mut delta = DeltaTextOperations::default();
|
||||||
delta.add(DeltaOperation::insert("123"));
|
delta.add(DeltaOperation::insert("123"));
|
||||||
|
|
||||||
let mut change = TextOperations::default();
|
let mut change = DeltaTextOperations::default();
|
||||||
change.add(DeltaOperation::retain(3));
|
change.add(DeltaOperation::retain(3));
|
||||||
change.add(DeltaOperation::insert("456"));
|
change.add(DeltaOperation::insert("456"));
|
||||||
let undo = change.invert(&delta);
|
let undo = change.invert(&delta);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use flowy_sync::client_document::{ClientDocument, EmptyDocument};
|
use flowy_sync::client_document::{ClientDocument, EmptyDocument};
|
||||||
use lib_ot::text_delta::TextOperation;
|
use lib_ot::text_delta::DeltaTextOperation;
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::*,
|
core::*,
|
||||||
text_delta::{BuildInTextAttribute, TextOperations},
|
text_delta::{BuildInTextAttribute, DeltaTextOperations},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -15,7 +15,7 @@ fn operation_insert_serialize_test() {
|
|||||||
let json = serde_json::to_string(&operation).unwrap();
|
let json = serde_json::to_string(&operation).unwrap();
|
||||||
eprintln!("{}", json);
|
eprintln!("{}", json);
|
||||||
|
|
||||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(insert_op, operation);
|
assert_eq!(insert_op, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,15 +24,15 @@ fn operation_retain_serialize_test() {
|
|||||||
let operation = DeltaOperation::Retain(12.into());
|
let operation = DeltaOperation::Retain(12.into());
|
||||||
let json = serde_json::to_string(&operation).unwrap();
|
let json = serde_json::to_string(&operation).unwrap();
|
||||||
eprintln!("{}", json);
|
eprintln!("{}", json);
|
||||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(insert_op, operation);
|
assert_eq!(insert_op, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn operation_delete_serialize_test() {
|
fn operation_delete_serialize_test() {
|
||||||
let operation = TextOperation::Delete(2);
|
let operation = DeltaTextOperation::Delete(2);
|
||||||
let json = serde_json::to_string(&operation).unwrap();
|
let json = serde_json::to_string(&operation).unwrap();
|
||||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(insert_op, operation);
|
assert_eq!(insert_op, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ fn delta_deserialize_test() {
|
|||||||
{"retain":2,"attributes":{"italic":true,"bold":true}},
|
{"retain":2,"attributes":{"italic":true,"bold":true}},
|
||||||
{"retain":2,"attributes":{"italic":true,"bold":true}}
|
{"retain":2,"attributes":{"italic":true,"bold":true}}
|
||||||
]"#;
|
]"#;
|
||||||
let delta = TextOperations::from_json(json).unwrap();
|
let delta = DeltaTextOperations::from_json(json).unwrap();
|
||||||
eprintln!("{}", delta);
|
eprintln!("{}", delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +86,12 @@ fn delta_deserialize_null_test() {
|
|||||||
let json = r#"[
|
let json = r#"[
|
||||||
{"retain":7,"attributes":{"bold":null}}
|
{"retain":7,"attributes":{"bold":null}}
|
||||||
]"#;
|
]"#;
|
||||||
let delta1 = TextOperations::from_json(json).unwrap();
|
let delta1 = DeltaTextOperations::from_json(json).unwrap();
|
||||||
|
|
||||||
let mut attribute = BuildInTextAttribute::Bold(true);
|
let mut attribute = BuildInTextAttribute::Bold(true);
|
||||||
attribute.remove_value();
|
attribute.remove_value();
|
||||||
|
|
||||||
let delta2 = OperationBuilder::new()
|
let delta2 = DeltaOperationBuilder::new()
|
||||||
.retain_with_attributes(7, attribute.into())
|
.retain_with_attributes(7, attribute.into())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
use crate::new_document::script::DocumentEditorTest;
|
||||||
|
use crate::new_document::script::EditScript::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn document_insert_h1_style_test() {
|
||||||
|
let scripts = vec![
|
||||||
|
ComposeTransactionStr {
|
||||||
|
transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"insert":"/"}],"inverted":[{"delete":1}]}],"after_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
|
||||||
|
},
|
||||||
|
AssertContent {
|
||||||
|
expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"/"}]}]}}"#,
|
||||||
|
},
|
||||||
|
ComposeTransactionStr {
|
||||||
|
transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"delete":1}],"inverted":[{"insert":"/"}]}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}}}"#,
|
||||||
|
},
|
||||||
|
ComposeTransactionStr {
|
||||||
|
transaction: r#"{"operations":[{"op":"update","path":[0,0],"attributes":{"subtype":"heading","heading":"h1"},"oldAttributes":{"subtype":null,"heading":null}}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
|
||||||
|
},
|
||||||
|
AssertContent {
|
||||||
|
expected: r#"{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"}}]}}"#,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
mod document_compose_test;
|
||||||
mod script;
|
mod script;
|
||||||
mod test;
|
mod test;
|
||||||
|
@ -1,17 +1,37 @@
|
|||||||
use flowy_document::editor::AppFlowyDocumentEditor;
|
use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransaction};
|
||||||
|
|
||||||
|
use flowy_document::entities::DocumentVersionPB;
|
||||||
use flowy_test::helper::ViewTest;
|
use flowy_test::helper::ViewTest;
|
||||||
use flowy_test::FlowySDKTest;
|
use flowy_test::FlowySDKTest;
|
||||||
use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
|
use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
|
||||||
use lib_ot::text_delta::TextOperations;
|
use lib_ot::text_delta::DeltaTextOperations;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub enum EditScript {
|
pub enum EditScript {
|
||||||
InsertText { path: Path, delta: TextOperations },
|
InsertText {
|
||||||
UpdateText { path: Path, delta: TextOperations },
|
path: Path,
|
||||||
Delete { path: Path },
|
delta: DeltaTextOperations,
|
||||||
AssertContent { expected: &'static str },
|
},
|
||||||
AssertPrettyContent { expected: &'static str },
|
UpdateText {
|
||||||
|
path: Path,
|
||||||
|
delta: DeltaTextOperations,
|
||||||
|
},
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ComposeTransaction {
|
||||||
|
transaction: Transaction,
|
||||||
|
},
|
||||||
|
ComposeTransactionStr {
|
||||||
|
transaction: &'static str,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
path: Path,
|
||||||
|
},
|
||||||
|
AssertContent {
|
||||||
|
expected: &'static str,
|
||||||
|
},
|
||||||
|
AssertPrettyContent {
|
||||||
|
expected: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DocumentEditorTest {
|
pub struct DocumentEditorTest {
|
||||||
@ -21,7 +41,8 @@ pub struct DocumentEditorTest {
|
|||||||
|
|
||||||
impl DocumentEditorTest {
|
impl DocumentEditorTest {
|
||||||
pub async fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
let sdk = FlowySDKTest::new(true);
|
let version = DocumentVersionPB::V1;
|
||||||
|
let sdk = FlowySDKTest::new(version.clone());
|
||||||
let _ = sdk.init_user().await;
|
let _ = sdk.init_user().await;
|
||||||
|
|
||||||
let test = ViewTest::new_document_view(&sdk).await;
|
let test = ViewTest::new_document_view(&sdk).await;
|
||||||
@ -62,6 +83,14 @@ impl DocumentEditorTest {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
EditScript::ComposeTransaction { transaction } => {
|
||||||
|
self.editor.apply_transaction(transaction).await.unwrap();
|
||||||
|
}
|
||||||
|
EditScript::ComposeTransactionStr { transaction } => {
|
||||||
|
let document_transaction = serde_json::from_str::<DocumentTransaction>(transaction).unwrap();
|
||||||
|
let transaction: Transaction = document_transaction.into();
|
||||||
|
self.editor.apply_transaction(transaction).await.unwrap();
|
||||||
|
}
|
||||||
EditScript::Delete { path } => {
|
EditScript::Delete { path } => {
|
||||||
let operation = NodeOperation::Delete { path, nodes: vec![] };
|
let operation = NodeOperation::Delete { path, nodes: vec![] };
|
||||||
self.editor
|
self.editor
|
||||||
@ -72,6 +101,9 @@ impl DocumentEditorTest {
|
|||||||
EditScript::AssertContent { expected } => {
|
EditScript::AssertContent { expected } => {
|
||||||
//
|
//
|
||||||
let content = self.editor.get_content(false).await.unwrap();
|
let content = self.editor.get_content(false).await.unwrap();
|
||||||
|
let expected_document: Document = serde_json::from_str(expected).unwrap();
|
||||||
|
let expected = serde_json::to_string(&expected_document).unwrap();
|
||||||
|
|
||||||
assert_eq!(content, expected);
|
assert_eq!(content, expected);
|
||||||
}
|
}
|
||||||
EditScript::AssertPrettyContent { expected } => {
|
EditScript::AssertPrettyContent { expected } => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::new_document::script::DocumentEditorTest;
|
use crate::new_document::script::DocumentEditorTest;
|
||||||
use crate::new_document::script::EditScript::*;
|
use crate::new_document::script::EditScript::*;
|
||||||
|
|
||||||
use lib_ot::text_delta::TextOperationBuilder;
|
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn document_initialize_test() {
|
async fn document_initialize_test() {
|
||||||
@ -13,7 +13,7 @@ async fn document_initialize_test() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn document_insert_text_test() {
|
async fn document_insert_text_test() {
|
||||||
let delta = TextOperationBuilder::new().insert("Hello world").build();
|
let delta = DeltaTextOperationBuilder::new().insert("Hello world").build();
|
||||||
let expected = r#"{
|
let expected = r#"{
|
||||||
"document": {
|
"document": {
|
||||||
"type": "editor",
|
"type": "editor",
|
||||||
@ -49,7 +49,7 @@ async fn document_update_text_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: vec![0, 0].into(),
|
path: vec![0, 0].into(),
|
||||||
delta: TextOperationBuilder::new().insert(&hello_world).build(),
|
delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
|
||||||
},
|
},
|
||||||
AssertPrettyContent {
|
AssertPrettyContent {
|
||||||
expected: r#"{
|
expected: r#"{
|
||||||
@ -75,7 +75,7 @@ async fn document_update_text_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: vec![0, 0].into(),
|
path: vec![0, 0].into(),
|
||||||
delta: TextOperationBuilder::new()
|
delta: DeltaTextOperationBuilder::new()
|
||||||
.retain(hello_world.len())
|
.retain(hello_world.len())
|
||||||
.insert(", AppFlowy")
|
.insert(", AppFlowy")
|
||||||
.build(),
|
.build(),
|
||||||
@ -122,11 +122,11 @@ async fn document_delete_text_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: vec![0, 0].into(),
|
path: vec![0, 0].into(),
|
||||||
delta: TextOperationBuilder::new().insert(&hello_world).build(),
|
delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
|
||||||
},
|
},
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: vec![0, 0].into(),
|
path: vec![0, 0].into(),
|
||||||
delta: TextOperationBuilder::new().retain(5).delete(6).build(),
|
delta: DeltaTextOperationBuilder::new().retain(5).delete(6).build(),
|
||||||
},
|
},
|
||||||
AssertPrettyContent { expected },
|
AssertPrettyContent { expected },
|
||||||
];
|
];
|
||||||
@ -139,7 +139,7 @@ async fn document_delete_node_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
UpdateText {
|
UpdateText {
|
||||||
path: vec![0, 0].into(),
|
path: vec![0, 0].into(),
|
||||||
delta: TextOperationBuilder::new().insert("Hello world").build(),
|
delta: DeltaTextOperationBuilder::new().insert("Hello world").build(),
|
||||||
},
|
},
|
||||||
AssertContent {
|
AssertContent {
|
||||||
expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"Hello world"}]}]}}"#,
|
expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"Hello world"}]}]}}"#,
|
||||||
|
@ -2,7 +2,7 @@ use flowy_document::old_editor::editor::DeltaDocumentEditor;
|
|||||||
use flowy_document::TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS;
|
use flowy_document::TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS;
|
||||||
use flowy_revision::disk::RevisionState;
|
use flowy_revision::disk::RevisionState;
|
||||||
use flowy_test::{helper::ViewTest, FlowySDKTest};
|
use flowy_test::{helper::ViewTest, FlowySDKTest};
|
||||||
use lib_ot::{core::Interval, text_delta::TextOperations};
|
use lib_ot::{core::Interval, text_delta::DeltaTextOperations};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ impl DeltaDocumentEditorTest {
|
|||||||
assert_eq!(next_revision.rev_id, rev_id.unwrap());
|
assert_eq!(next_revision.rev_id, rev_id.unwrap());
|
||||||
}
|
}
|
||||||
EditorScript::AssertJson(expected) => {
|
EditorScript::AssertJson(expected) => {
|
||||||
let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
|
let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||||
let delta = self.editor.document_operations().await.unwrap();
|
let delta = self.editor.document_operations().await.unwrap();
|
||||||
if expected_delta != delta {
|
if expected_delta != delta {
|
||||||
eprintln!("✅ expect: {}", expected,);
|
eprintln!("✅ expect: {}", expected,);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user