mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: integrate new editor (#2536)
* feat: update editor * feat: integrate new editor * feat: integrate the document2 into folder2 * feat: integrate the new editor * chore: cargo fix * chore: active document feature for flowy-error * feat: convert the editor action to collab action * feat: migrate the grid and board * feat: migrate the callout block * feat: migrate the divider * chore: migrate the callout and math equation * feat: migrate the code block * feat: add shift + enter command in code block * feat: support tab and shift+tab in code block * fix: cursor error after inserting divider * feat: migrate the grid and board * feat: migrate the emoji picker * feat: migrate openai * feat: integrate floating toolbar * feat: migrate the smart editor * feat: migrate the cover * feat: add option block action * chore: implement block selection and delete option * feat: support background color * feat: dismiss color picker afer setting color * feat: migrate the cover block * feat: resize the font * chore: cutomsize the padding * chore: wrap the option button with ignore widget * feat: customize the heading style * chore: optimize the divider line height * fix: the option button can't dismiss * ci: rust test * chore: flutter analyze * fix: code block selection * fix: dismiss the emoji picker after selecting one * chore: optimize the transaction adapter * fix: can't save the new content * feat: show export page when some errors happen * feat: implement get_view_data for document --------- Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
99c48f7100
commit
2202326278
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M9.41 7.3H9.4M14.6 7.3h-.01M9.31 12H9.3M14.6 12h-.01M9.41 16.7H9.4M14.6 16.7h-.01"/></svg>
|
After Width: | Height: | Size: 238 B |
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:async';
|
||||
|
||||
|
@ -66,7 +66,12 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
|
||||
if (showOptions) {
|
||||
cells.add(const TypeOptionSeparator());
|
||||
cells.add(
|
||||
SelectOptionColorList(selectedColor: state.option.color),
|
||||
SelectOptionColorList(
|
||||
selectedColor: state.option.color,
|
||||
onSelectedColor: (color) => context
|
||||
.read<EditSelectOptionBloc>()
|
||||
.add(EditSelectOptionEvent.updateColor(color)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -147,16 +152,22 @@ class _OptionNameTextField extends StatelessWidget {
|
||||
}
|
||||
|
||||
class SelectOptionColorList extends StatelessWidget {
|
||||
final SelectOptionColorPB selectedColor;
|
||||
const SelectOptionColorList({required this.selectedColor, Key? key})
|
||||
: super(key: key);
|
||||
const SelectOptionColorList({
|
||||
super.key,
|
||||
this.selectedColor,
|
||||
required this.onSelectedColor,
|
||||
});
|
||||
|
||||
final SelectOptionColorPB? selectedColor;
|
||||
final void Function(SelectOptionColorPB color) onSelectedColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = SelectOptionColorPB.values.map((color) {
|
||||
return _SelectOptionColorCell(
|
||||
color: color,
|
||||
isSelected: selectedColor == color,
|
||||
isSelected: selectedColor != null ? selectedColor == color : false,
|
||||
onSelectedColor: onSelectedColor,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@ -193,14 +204,17 @@ class SelectOptionColorList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionColorCell extends StatelessWidget {
|
||||
final SelectOptionColorPB color;
|
||||
final bool isSelected;
|
||||
const _SelectOptionColorCell({
|
||||
required this.color,
|
||||
required this.isSelected,
|
||||
required this.onSelectedColor,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final SelectOptionColorPB color;
|
||||
final bool isSelected;
|
||||
final void Function(SelectOptionColorPB color) onSelectedColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget? checkmark;
|
||||
@ -228,11 +242,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
),
|
||||
leftIcon: colorIcon,
|
||||
rightIcon: checkmark,
|
||||
onTap: () {
|
||||
context
|
||||
.read<EditSelectOptionBloc>()
|
||||
.add(EditSelectOptionEvent.updateColor(color));
|
||||
},
|
||||
onTap: () => onSelectedColor(color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,48 +1,71 @@
|
||||
import 'dart:convert';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/util/json_print.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show EditorState, Document, Transaction, Node;
|
||||
show EditorState, LogLevel;
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/util/either_extension.dart';
|
||||
part 'doc_bloc.freezed.dart';
|
||||
|
||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final ViewPB view;
|
||||
final DocumentService _documentService;
|
||||
final DocumentListener _docListener;
|
||||
DocumentBloc({
|
||||
required this.view,
|
||||
}) : _documentListener = DocumentListener(id: view.id),
|
||||
_viewListener = ViewListener(view: view),
|
||||
_documentService = DocumentService(),
|
||||
_trashService = TrashService(),
|
||||
super(DocumentState.initial()) {
|
||||
_transactionAdapter = TransactionAdapter(
|
||||
documentId: view.id,
|
||||
documentService: _documentService,
|
||||
);
|
||||
on<DocumentEvent>(_onDocumentEvent);
|
||||
}
|
||||
|
||||
final ViewListener _listener;
|
||||
final ViewPB view;
|
||||
|
||||
final DocumentListener _documentListener;
|
||||
final ViewListener _viewListener;
|
||||
|
||||
final DocumentService _documentService;
|
||||
final TrashService _trashService;
|
||||
|
||||
late final TransactionAdapter _transactionAdapter;
|
||||
|
||||
EditorState? editorState;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
DocumentBloc({
|
||||
required this.view,
|
||||
}) : _documentService = DocumentService(),
|
||||
_docListener = DocumentListener(id: view.id),
|
||||
_listener = ViewListener(view: view),
|
||||
_trashService = TrashService(),
|
||||
super(DocumentState.initial()) {
|
||||
on<DocumentEvent>((event, emit) async {
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _viewListener.stop();
|
||||
await _subscription?.cancel();
|
||||
await _documentService.closeDocument(view: view);
|
||||
editorState?.cancelSubscription();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onDocumentEvent(
|
||||
DocumentEvent event,
|
||||
Emitter<DocumentState> emit,
|
||||
) async {
|
||||
await event.map(
|
||||
initial: (Initial value) async {
|
||||
_listenOnDocChange();
|
||||
await _initial(value, emit);
|
||||
_listenOnViewChange();
|
||||
final state = await _fetchDocumentState();
|
||||
await _subscribe(state);
|
||||
emit(state);
|
||||
},
|
||||
deleted: (Deleted value) async {
|
||||
emit(state.copyWith(isDeleted: true));
|
||||
@ -52,121 +75,87 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
},
|
||||
deletePermanently: (DeletePermanently value) async {
|
||||
final result = await _trashService.deleteViews([view.id]);
|
||||
|
||||
final newState = result.fold(
|
||||
(l) => state.copyWith(forceClose: true),
|
||||
(r) => state,
|
||||
);
|
||||
emit(newState);
|
||||
emit(state.copyWith(forceClose: result.swap().isLeft()));
|
||||
},
|
||||
restorePage: (RestorePage value) async {
|
||||
final result = await _trashService.putback(view.id);
|
||||
final newState = result.fold(
|
||||
(l) => state.copyWith(isDeleted: false),
|
||||
(r) => state,
|
||||
);
|
||||
emit(newState);
|
||||
emit(state.copyWith(isDeleted: result.swap().isRight()));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
Future<void> _subscribe(DocumentState state) async {
|
||||
_onViewChanged();
|
||||
_onDocumentChanged();
|
||||
|
||||
if (_subscription != null) {
|
||||
await _subscription?.cancel();
|
||||
// create the editor state
|
||||
await state.loadingState.whenOrNull(
|
||||
finish: (data) async => data.map((r) {
|
||||
_initAppFlowyEditorState(r);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await _documentService.closeDocument(docId: view.id);
|
||||
await _documentService.closeDocumentV2(view: view);
|
||||
return super.close();
|
||||
/// subscribe to the view(document page) change
|
||||
void _onViewChanged() {
|
||||
_viewListener.start(
|
||||
onViewDeleted: (r) =>
|
||||
r.swap().map((r) => add(const DocumentEvent.deleted())),
|
||||
onViewRestored: (r) =>
|
||||
r.swap().map((r) => add(const DocumentEvent.restore())),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
||||
final userProfile = await UserBackendService.getCurrentUserProfile();
|
||||
if (userProfile.isRight()) {
|
||||
return emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(
|
||||
right(userProfile.asRight()),
|
||||
),
|
||||
/// subscribe to the document content change
|
||||
void _onDocumentChanged() {
|
||||
_documentListener.start(
|
||||
didReceiveUpdate: (docEvent) {
|
||||
// todo: integrate the document change to the editor
|
||||
// prettyPrintJson(docEvent.toProto3Json());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch document
|
||||
Future<DocumentState> _fetchDocumentState() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile().then(
|
||||
(value) async => value.andThen(
|
||||
// open the document
|
||||
await _documentService.openDocument(view: view),
|
||||
),
|
||||
);
|
||||
}
|
||||
final result = await _documentService.openDocument(view: view);
|
||||
|
||||
return result.fold(
|
||||
(documentData) async {
|
||||
await _initEditorState(documentData).whenComplete(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(left(unit)),
|
||||
userProfilePB: userProfile.asLeft(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
(err) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(right(err)),
|
||||
),
|
||||
);
|
||||
},
|
||||
return state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(result),
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnViewChange() {
|
||||
_listener.start(
|
||||
onViewDeleted: (result) {
|
||||
result.fold(
|
||||
(view) => add(const DocumentEvent.deleted()),
|
||||
(error) {},
|
||||
);
|
||||
},
|
||||
onViewRestored: (result) {
|
||||
result.fold(
|
||||
(view) => add(const DocumentEvent.restore()),
|
||||
(error) {},
|
||||
);
|
||||
},
|
||||
);
|
||||
Future<void> _initAppFlowyEditorState(DocumentDataPB2 data) async {
|
||||
if (kDebugMode) {
|
||||
prettyPrintJson(data.toProto3Json());
|
||||
}
|
||||
|
||||
void _listenOnDocChange() {
|
||||
_docListener.start(
|
||||
didReceiveUpdate: () {},
|
||||
);
|
||||
final document = data.toDocument();
|
||||
if (document == null) {
|
||||
assert(false, 'document is null');
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _initEditorState(DocumentDataPB documentData) async {
|
||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
||||
final editorState = EditorState(document: document);
|
||||
this.editorState = editorState;
|
||||
|
||||
// listen on document change
|
||||
_subscription = editorState.transactionStream.listen((transaction) {
|
||||
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
||||
_documentService
|
||||
.applyEdit(docId: view.id, operations: json)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
// subscribe to the document change from the editor
|
||||
_subscription = editorState.transactionStream.listen((transaction) async {
|
||||
await _transactionAdapter.apply(transaction, editorState);
|
||||
});
|
||||
});
|
||||
// log
|
||||
|
||||
// output the log from the editor when debug mode
|
||||
if (kDebugMode) {
|
||||
editorState.logConfiguration.handler = (log) {
|
||||
editorState.logConfiguration
|
||||
..level = LogLevel.all
|
||||
..handler = (log) {
|
||||
Log.debug(log);
|
||||
};
|
||||
}
|
||||
// migration
|
||||
final migration = DocumentMigration(editorState: editorState);
|
||||
await migration.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,77 +189,6 @@ class DocumentState with _$DocumentState {
|
||||
class DocumentLoadingState with _$DocumentLoadingState {
|
||||
const factory DocumentLoadingState.loading() = _Loading;
|
||||
const factory DocumentLoadingState.finish(
|
||||
Either<Unit, FlowyError> successOrFail,
|
||||
Either<FlowyError, DocumentDataPB2> 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;
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentMigration {
|
||||
const DocumentMigration({
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
/// Migrate the document to the latest version.
|
||||
Future<void> apply() async {
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// A temporary solution to migrate the document to the latest version.
|
||||
// Once the editor is stable, we can remove this.
|
||||
|
||||
// cover plugin
|
||||
if (editorState.document.nodeAtPath([0])?.type != kCoverType) {
|
||||
transaction.insertNode(
|
||||
[0],
|
||||
Node(type: kCoverType),
|
||||
);
|
||||
}
|
||||
|
||||
transaction.afterSelection = null;
|
||||
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,69 +3,50 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
|
||||
class DocumentService {
|
||||
Future<Either<DocumentDataPB, FlowyError>> openDocument({
|
||||
// unused now.
|
||||
Future<Either<FlowyError, Unit>> createDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||
|
||||
final payload = OpenDocumentPayloadPB()
|
||||
..documentId = view.id
|
||||
..version = DocumentVersionPB.V1;
|
||||
// switch (view.dataFormat) {
|
||||
// case ViewDataFormatPB.DeltaFormat:
|
||||
// payload.documentVersion = DocumentVersionPB.V0;
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
|
||||
return DocumentEventGetDocument(payload).send();
|
||||
final canOpen = await openDocument(view: view);
|
||||
if (canOpen.isRight()) {
|
||||
return const Right(unit);
|
||||
}
|
||||
final payload = CreateDocumentPayloadPBV2()..documentId = view.id;
|
||||
final result = await DocumentEvent2CreateDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> applyEdit({
|
||||
required String docId,
|
||||
required String operations,
|
||||
}) {
|
||||
final payload = EditPayloadPB.create()
|
||||
..docId = docId
|
||||
..operations = operations;
|
||||
return DocumentEventApplyEdit(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
|
||||
final payload = ViewIdPB(value: docId);
|
||||
return FolderEventCloseView(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<DocumentDataPB2, FlowyError>> openDocumentV2({
|
||||
Future<Either<FlowyError, DocumentDataPB2>> openDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
// set the latest view
|
||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||
|
||||
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
|
||||
|
||||
return DocumentEvent2OpenDocument(payload).send();
|
||||
final result = await DocumentEvent2OpenDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> closeDocumentV2({
|
||||
Future<Either<FlowyError, Unit>> closeDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
|
||||
return DocumentEvent2CloseDocument(payload).send();
|
||||
final result = await DocumentEvent2CloseDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> applyAction({
|
||||
required ViewPB view,
|
||||
required List<BlockActionPB> actions,
|
||||
Future<Either<FlowyError, Unit>> applyAction({
|
||||
required String documentId,
|
||||
required Iterable<BlockActionPB> actions,
|
||||
}) async {
|
||||
final payload = ApplyActionPayloadPBV2(
|
||||
documentId: view.id,
|
||||
documentId: documentId,
|
||||
actions: actions,
|
||||
);
|
||||
return DocumentEvent2ApplyAction(payload).send();
|
||||
final result = await DocumentEvent2ApplyAction(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,93 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show Document, Node, Attributes, Delta, ParagraphBlockKeys;
|
||||
|
||||
extension AppFlowyEditor on DocumentDataPB2 {
|
||||
Document? toDocument() {
|
||||
final rootId = pageId;
|
||||
try {
|
||||
final root = buildNode(rootId);
|
||||
return Document(root: root);
|
||||
} catch (e) {
|
||||
Log.error('create document error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Node buildNode(String id) {
|
||||
final block = blocks[id]!; // TODO: don't use force unwrap
|
||||
final childrenId = block.childrenId;
|
||||
final childrenIds = meta.childrenMap[childrenId]?.children;
|
||||
final children = <Node>[];
|
||||
if (childrenIds != null && childrenIds.isNotEmpty) {
|
||||
children.addAll(childrenIds.map((e) => buildNode(e)));
|
||||
}
|
||||
return block.toNode(children: children);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackendKeys {
|
||||
const _BackendKeys._();
|
||||
|
||||
static const String page = 'page';
|
||||
static const String text = 'text';
|
||||
}
|
||||
|
||||
extension BlockToNode on BlockPB {
|
||||
Node toNode({
|
||||
Iterable<Node>? children,
|
||||
}) {
|
||||
return Node(
|
||||
id: id,
|
||||
type: _typeAdapter(ty),
|
||||
attributes: _dataAdapter(ty, data),
|
||||
children: children ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
String _typeAdapter(String ty) {
|
||||
final adapter = {
|
||||
_BackendKeys.page: 'document',
|
||||
_BackendKeys.text: ParagraphBlockKeys.type,
|
||||
};
|
||||
return adapter[ty] ?? ty;
|
||||
}
|
||||
|
||||
Attributes _dataAdapter(String ty, String data) {
|
||||
final map = Attributes.from(jsonDecode(data));
|
||||
final adapter = {
|
||||
'text': (Attributes map) => map
|
||||
..putIfAbsent(
|
||||
'delta',
|
||||
() => Delta().toJson(),
|
||||
),
|
||||
};
|
||||
return adapter[ty]?.call(map) ?? map;
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeToBlock on Node {
|
||||
BlockPB toBlock() {
|
||||
assert(id.isNotEmpty);
|
||||
final block = BlockPB.create()
|
||||
..id = id
|
||||
..ty = _typeAdapter(type)
|
||||
..data = _dataAdapter(type, attributes);
|
||||
return block;
|
||||
}
|
||||
|
||||
String _typeAdapter(String type) {
|
||||
final adapter = {
|
||||
'document': 'page',
|
||||
'paragraph': 'text',
|
||||
};
|
||||
return adapter[type] ?? type;
|
||||
}
|
||||
|
||||
String _dataAdapter(String type, Attributes attributes) {
|
||||
return jsonEncode(attributes);
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show
|
||||
EditorState,
|
||||
Transaction,
|
||||
Operation,
|
||||
InsertOperation,
|
||||
UpdateOperation,
|
||||
DeleteOperation,
|
||||
PathExtensions,
|
||||
Node;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
|
||||
/// Uses to adjust the data structure between the editor and the backend.
|
||||
///
|
||||
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
|
||||
/// This adapter is used to convert the editor's transaction to the backend's transaction.
|
||||
class TransactionAdapter {
|
||||
TransactionAdapter({
|
||||
required this.documentId,
|
||||
required this.documentService,
|
||||
});
|
||||
|
||||
final DocumentService documentService;
|
||||
final String documentId;
|
||||
|
||||
Future<void> apply(Transaction transaction, EditorState editorState) async {
|
||||
final actions = transaction.operations
|
||||
.map((op) => op.toBlockAction(editorState))
|
||||
.whereNotNull()
|
||||
.expand((element) => element);
|
||||
Log.debug('actions => $actions');
|
||||
await documentService.applyAction(
|
||||
documentId: documentId,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Operation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final op = this;
|
||||
if (op is InsertOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
} else if (op is UpdateOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
} else if (op is DeleteOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
}
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
extension on InsertOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
// store the previous node for continuous insertion.
|
||||
// because the backend needs to know the previous node's id.
|
||||
Node? previousNode;
|
||||
for (final node in nodes) {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
final prevId = previousNode?.id ??
|
||||
node.previous?.id ??
|
||||
editorState.getNodeAtPath(path.previous)?.id ??
|
||||
'';
|
||||
assert(parentId.isNotEmpty && prevId.isNotEmpty);
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock()
|
||||
..parentId = parentId
|
||||
..prevId = prevId;
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..action = BlockActionTypePB.Insert
|
||||
..payload = payload,
|
||||
);
|
||||
previousNode = node;
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
extension on UpdateOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
|
||||
// if the attributes are both empty, we don't need to update
|
||||
if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {
|
||||
return actions;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(path);
|
||||
if (node == null) {
|
||||
assert(false, 'node not found at path: $path');
|
||||
return actions;
|
||||
}
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
assert(parentId.isNotEmpty);
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock()
|
||||
..parentId = parentId;
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..action = BlockActionTypePB.Update
|
||||
..payload = payload,
|
||||
);
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeleteOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
for (final node in nodes) {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock()
|
||||
..parentId = parentId;
|
||||
assert(parentId.isNotEmpty);
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..action = BlockActionTypePB.Delete
|
||||
..payload = payload,
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/plugins/document/application/share_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
|
@ -1,41 +1,51 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/base64_string.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../startup/startup.dart';
|
||||
import 'application/doc_bloc.dart';
|
||||
import 'editor_styles.dart';
|
||||
import 'presentation/banner.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
const DocumentPage({
|
||||
super.key,
|
||||
required this.onDeleted,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final VoidCallback onDeleted;
|
||||
final ViewPB view;
|
||||
|
||||
DocumentPage({
|
||||
required this.view,
|
||||
required this.onDeleted,
|
||||
Key? key,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
|
||||
@override
|
||||
State<DocumentPage> createState() => _DocumentPageState();
|
||||
}
|
||||
|
||||
class _DocumentPageState extends State<DocumentPage> {
|
||||
late DocumentBloc documentBloc;
|
||||
late final DocumentBloc documentBloc;
|
||||
EditorState? editorState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
documentBloc = getIt<DocumentBloc>(param1: widget.view)
|
||||
..add(const DocumentEvent.initial());
|
||||
|
||||
// The appflowy editor use Intl as localization, set the default language as fallback.
|
||||
Intl.defaultLocale = 'en_US';
|
||||
documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
|
||||
..add(const DocumentEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -46,28 +56,29 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DocumentBloc>.value(value: documentBloc),
|
||||
],
|
||||
return BlocProvider.value(
|
||||
value: documentBloc,
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) => SizedBox.expand(
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) {
|
||||
return state.loadingState.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
finish: (result) => result.fold(
|
||||
(error) => FlowyErrorPage(error.toString()),
|
||||
(data) {
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
} else if (documentBloc.editorState == null) {
|
||||
return const SizedBox();
|
||||
return Center(
|
||||
child: ExportPageWidget(
|
||||
onTap: () async => await _exportPage(data),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _renderDocument(context, state);
|
||||
editorState = documentBloc.editorState!;
|
||||
return _buildEditorPage(context, state);
|
||||
}
|
||||
},
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -75,177 +86,61 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderDocument(BuildContext context, DocumentState state) {
|
||||
Widget _buildEditorPage(BuildContext context, DocumentState state) {
|
||||
final appflowyEditorPage = AppFlowyEditorPage(
|
||||
editorState: editorState!,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
if (state.isDeleted) _renderBanner(context),
|
||||
// AppFlowy Editor
|
||||
const _AppFlowyEditorPage(),
|
||||
if (state.isDeleted) _buildBanner(context),
|
||||
_buildCoverAndIcon(context),
|
||||
Expanded(
|
||||
child: appflowyEditorPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderBanner(BuildContext context) {
|
||||
Widget _buildBanner(BuildContext context) {
|
||||
return DocumentBanner(
|
||||
onRestore: () =>
|
||||
context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
|
||||
onDelete: () => context
|
||||
.read<DocumentBloc>()
|
||||
.add(const DocumentEvent.deletePermanently()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyEditorPage extends StatefulWidget {
|
||||
const _AppFlowyEditorPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||
}
|
||||
|
||||
class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
||||
late DocumentBloc documentBloc;
|
||||
late EditorState editorState;
|
||||
String? get openAIKey => documentBloc.state.userProfilePB?.openaiKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
documentBloc = context.read<DocumentBloc>();
|
||||
editorState = documentBloc.editorState ?? EditorState.empty();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final autoFocusParameters = _autoFocusParameters();
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
autoFocus: autoFocusParameters.value1,
|
||||
focusedSelection: autoFocusParameters.value2,
|
||||
customBuilders: {
|
||||
// Divider
|
||||
kDividerType: DividerWidgetBuilder(),
|
||||
// Math Equation
|
||||
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
||||
// Code Block
|
||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
||||
// Board
|
||||
kBoardType: BoardNodeWidgetBuilder(),
|
||||
// Grid
|
||||
kGridType: GridNodeWidgetBuilder(),
|
||||
// Card
|
||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
||||
// Auto Generator,
|
||||
kAutoCompletionInputType: AutoCompletionInputBuilder(),
|
||||
// Cover
|
||||
kCoverType: CoverNodeWidgetBuilder(),
|
||||
// Smart Edit,
|
||||
kSmartEditType: SmartEditInputBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
// Divider
|
||||
insertDividerEvent,
|
||||
// Code Block
|
||||
enterInCodeBlock,
|
||||
ignoreKeysInCodeBlock,
|
||||
pasteInCodeBlock,
|
||||
],
|
||||
selectionMenuItems: [
|
||||
// Divider
|
||||
dividerMenuItem,
|
||||
// Math Equation
|
||||
mathEquationMenuItem,
|
||||
// Code Block
|
||||
codeBlockMenuItem,
|
||||
// Emoji
|
||||
emojiMenuItem,
|
||||
// Board
|
||||
boardMenuItem,
|
||||
// Create Board
|
||||
boardViewMenuItem(documentBloc),
|
||||
// Grid
|
||||
gridMenuItem,
|
||||
// Create Grid
|
||||
gridViewMenuItem(documentBloc),
|
||||
// Callout
|
||||
calloutMenuItem,
|
||||
// AI
|
||||
// enable open ai features if needed.
|
||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
||||
autoGeneratorMenuItem,
|
||||
],
|
||||
],
|
||||
toolbarItems: [
|
||||
smartEditItem,
|
||||
],
|
||||
themeData: theme.copyWith(
|
||||
extensions: [
|
||||
...theme.extensions.values,
|
||||
customEditorTheme(context),
|
||||
...customPluginTheme(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
onRestore: () => documentBloc.add(const DocumentEvent.restorePage()),
|
||||
onDelete: () => documentBloc.add(const DocumentEvent.deletePermanently()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clearTemporaryNodes();
|
||||
super.dispose();
|
||||
Widget _buildCoverAndIcon(BuildContext context) {
|
||||
if (editorState == null) {
|
||||
return const Placeholder();
|
||||
}
|
||||
final page = editorState!.document.root;
|
||||
return CoverImageNodeWidget(
|
||||
node: page,
|
||||
editorState: editorState!,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearTemporaryNodes() async {
|
||||
final document = editorState.document;
|
||||
if (document.root.children.isEmpty) {
|
||||
Future<void> _exportPage(DocumentDataPB2 data) async {
|
||||
final picker = getIt<FilePickerService>();
|
||||
final dir = await picker.getDirectoryPath();
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
final temporaryNodeTypes = [
|
||||
kAutoCompletionInputType,
|
||||
kSmartEditType,
|
||||
];
|
||||
final iterator = NodeIterator(
|
||||
document: document,
|
||||
startNode: document.root.children.first,
|
||||
);
|
||||
final transaction = editorState.transaction;
|
||||
while (iterator.moveNext()) {
|
||||
final node = iterator.current;
|
||||
if (temporaryNodeTypes.contains(node.type)) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
if (kCoverType == node.type && !node.path.equals([0])) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
}
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
await editorState.apply(transaction, withUpdateCursor: false);
|
||||
}
|
||||
final path = p.join(dir, '${documentBloc.view.name}.json');
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
final json = encoder.convert(data.toProto3Json());
|
||||
await File(path).writeAsString(json.base64.base64);
|
||||
|
||||
_showMessage('Export success to $path');
|
||||
}
|
||||
|
||||
dartz.Tuple2<bool, Selection?> _autoFocusParameters() {
|
||||
if (editorState.document.isEmpty) {
|
||||
return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
|
||||
void _showMessage(String message) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final texts = editorState.document.root.children.whereType<TextNode>();
|
||||
if (texts.every((element) => element.toPlainText().isEmpty)) {
|
||||
return dartz.Tuple2(
|
||||
true,
|
||||
Selection.single(path: texts.first.path, startOffset: 0),
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const dartz.Tuple2(false, null);
|
||||
}
|
||||
}
|
||||
|
@ -1,124 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
EditorStyle customEditorTheme(BuildContext context) {
|
||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
var editorStyle = EditorStyle(
|
||||
// Editor styles
|
||||
padding: const EdgeInsets.symmetric(horizontal: 100),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
cursorColor: theme.colorScheme.primary,
|
||||
// Text styles
|
||||
textPadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: TextStyle(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: documentStyle.fontSize,
|
||||
color: theme.colorScheme.onBackground,
|
||||
),
|
||||
selectionColor: theme.colorScheme.tertiary.withOpacity(0.2),
|
||||
// Selection menu
|
||||
selectionMenuBackgroundColor: theme.cardColor,
|
||||
selectionMenuItemTextColor: theme.iconTheme.color,
|
||||
selectionMenuItemIconColor: theme.colorScheme.onBackground,
|
||||
selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,
|
||||
selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,
|
||||
selectionMenuItemSelectedColor: theme.hoverColor,
|
||||
// Toolbar and its item's style
|
||||
toolbarColor: theme.colorScheme.onTertiary,
|
||||
toolbarElevation: 0,
|
||||
lineHeight: 1.5,
|
||||
placeholderTextStyle:
|
||||
TextStyle(fontSize: documentStyle.fontSize, color: theme.hintColor),
|
||||
bold: const TextStyle(
|
||||
fontFamily: 'poppins-Bold',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
||||
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||
href: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
highlightColorHex: '0x6000BCF0',
|
||||
code: GoogleFonts.robotoMono(
|
||||
textStyle: TextStyle(
|
||||
fontSize: documentStyle.fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.red,
|
||||
backgroundColor: theme.colorScheme.inverseSurface,
|
||||
),
|
||||
),
|
||||
popupMenuFGColor: theme.iconTheme.color,
|
||||
popupMenuHoverColor: theme.colorScheme.tertiaryContainer,
|
||||
);
|
||||
|
||||
return editorStyle;
|
||||
}
|
||||
|
||||
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
||||
final baseFontSize = documentStyle.fontSize;
|
||||
const basePadding = 12.0;
|
||||
var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? HeadingPluginStyle.dark
|
||||
: HeadingPluginStyle.light;
|
||||
headingPluginStyle = headingPluginStyle.copyWith(
|
||||
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);
|
||||
},
|
||||
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);
|
||||
},
|
||||
);
|
||||
var numberListPluginStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? NumberListPluginStyle.dark
|
||||
: NumberListPluginStyle.light;
|
||||
|
||||
numberListPluginStyle = numberListPluginStyle.copyWith(
|
||||
icon: (_, textNode) {
|
||||
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
|
||||
return Container(
|
||||
padding: iconPadding,
|
||||
child: Text(
|
||||
'${textNode.attributes.number.toString()}.',
|
||||
style: customEditorTheme(context).textStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
final pluginTheme = Theme.of(context).brightness == Brightness.dark
|
||||
? darkPluginStyleExtension
|
||||
: lightPluginStyleExtension;
|
||||
return pluginTheme.toList()
|
||||
..removeWhere(
|
||||
(element) =>
|
||||
element is HeadingPluginStyle || element is NumberListPluginStyle,
|
||||
)
|
||||
..add(headingPluginStyle)
|
||||
..add(numberListPluginStyle);
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// Wrapper for the appflowy editor.
|
||||
class AppFlowyEditorPage extends StatefulWidget {
|
||||
const AppFlowyEditorPage({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||
}
|
||||
|
||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final scrollController = ScrollController();
|
||||
final slashMenuItems = [
|
||||
boardMenuItem,
|
||||
gridMenuItem,
|
||||
calloutItem,
|
||||
dividerMenuItem,
|
||||
mathEquationItem,
|
||||
codeBlockItem,
|
||||
emojiMenuItem,
|
||||
autoGeneratorMenuItem,
|
||||
];
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
...codeBlockCommands,
|
||||
...standardCommandShortcutEvents,
|
||||
];
|
||||
|
||||
final List<ToolbarItem> toolbarItems = [
|
||||
smartEditItem,
|
||||
placeholderItem,
|
||||
paragraphItem,
|
||||
...headingItems,
|
||||
placeholderItem,
|
||||
...markdownFormatItems,
|
||||
placeholderItem,
|
||||
quoteItem,
|
||||
bulletedListItem,
|
||||
numberedListItem,
|
||||
placeholderItem,
|
||||
linkItem,
|
||||
textColorItem,
|
||||
highlightColorItem,
|
||||
];
|
||||
|
||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||
_customAppFlowyBlockComponentBuilders();
|
||||
late final List<CharacterShortcutEvent> characterShortcutEvents = [
|
||||
// divider
|
||||
convertMinusesToDivider,
|
||||
|
||||
// code block
|
||||
...codeBlockCharacterEvents,
|
||||
|
||||
...standardCharacterShortcutEvents
|
||||
..removeWhere(
|
||||
(element) => element == slashCommand,
|
||||
), // remove the default slash command.
|
||||
customSlashCommand(slashMenuItems),
|
||||
];
|
||||
|
||||
late final styleCustomizer = EditorStyleCustomizer(context: context);
|
||||
DocumentBloc get documentBloc => context.read<DocumentBloc>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final autoFocusParameters = _computeAutoFocusParameters();
|
||||
final editor = AppFlowyEditor.custom(
|
||||
editorState: widget.editorState,
|
||||
editable: true,
|
||||
scrollController: scrollController,
|
||||
// setup the auto focus parameters
|
||||
autoFocus: autoFocusParameters.item1,
|
||||
focusedSelection: autoFocusParameters.item2,
|
||||
// setup the theme
|
||||
editorStyle: styleCustomizer.style(),
|
||||
// customize the block builder
|
||||
blockComponentBuilders: blockComponentBuilders,
|
||||
// customize the shortcuts
|
||||
characterShortcutEvents: characterShortcutEvents,
|
||||
commandShortcutEvents: commandShortcutEvents,
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
child: FloatingToolbar(
|
||||
items: toolbarItems,
|
||||
editorState: widget.editorState,
|
||||
scrollController: scrollController,
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
OptionAction.divider,
|
||||
OptionAction.moveUp,
|
||||
OptionAction.moveDown,
|
||||
];
|
||||
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 4.0),
|
||||
);
|
||||
final customBlockComponentBuilderMap = {
|
||||
'document': DocumentComponentBuilder(),
|
||||
ParagraphBlockKeys.type: TextBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'To-do',
|
||||
),
|
||||
),
|
||||
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'Quote',
|
||||
),
|
||||
),
|
||||
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
placeholderText: (node) =>
|
||||
'Heading ${node.attributes[HeadingBlockKeys.level]}',
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
),
|
||||
ImageBlockKeys.type: ImageBlockComponentBuilder(),
|
||||
BoardBlockKeys.type: BoardBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
GridBlockKeys.type: GridBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(),
|
||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
||||
),
|
||||
),
|
||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 30,
|
||||
right: 30,
|
||||
bottom: 36,
|
||||
),
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
};
|
||||
|
||||
final builders = {
|
||||
...standardBlockComponentBuilderMap,
|
||||
...customBlockComponentBuilderMap,
|
||||
};
|
||||
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == 'document') {
|
||||
continue;
|
||||
}
|
||||
final builder = entry.value;
|
||||
|
||||
// customize the action builder.
|
||||
final supportColorBuilderTypes = [
|
||||
ParagraphBlockKeys.type,
|
||||
HeadingBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type
|
||||
];
|
||||
if (!supportColorBuilderTypes.contains(entry.key)) {
|
||||
builder.actionBuilder = (context, state) => OptionActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: widget.editorState,
|
||||
actions: standardActions,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
final colorAction = [
|
||||
OptionAction.divider,
|
||||
OptionAction.color,
|
||||
];
|
||||
builder.actionBuilder = (context, state) => OptionActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: widget.editorState,
|
||||
actions: standardActions + colorAction,
|
||||
);
|
||||
}
|
||||
|
||||
return builders;
|
||||
}
|
||||
|
||||
Tuple2<bool, Selection?> _computeAutoFocusParameters() {
|
||||
if (widget.editorState.document.isEmpty) {
|
||||
return Tuple2(true, Selection.collapse([0], 0));
|
||||
}
|
||||
final nodes = widget.editorState.document.root.children
|
||||
.where((element) => element.delta != null);
|
||||
final isAllEmpty =
|
||||
nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
|
||||
if (isAllEmpty) {
|
||||
return Tuple2(true, Selection.collapse(nodes.first.path, 0));
|
||||
}
|
||||
return const Tuple2(false, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum OptionAction {
|
||||
delete,
|
||||
duplicate,
|
||||
turnInto,
|
||||
moveUp,
|
||||
moveDown,
|
||||
color,
|
||||
divider,
|
||||
}
|
||||
|
||||
class DividerOptionAction extends CustomActionCell {
|
||||
@override
|
||||
Widget buildWithContext(BuildContext context) {
|
||||
return const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorOptionAction extends PopoverActionCell {
|
||||
ColorOptionAction({
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) {
|
||||
return svgWidget(
|
||||
'editor/delete', // todo: add color icon
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get name {
|
||||
return 'Color'; // todo: l10n
|
||||
}
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext context, PopoverController controller)
|
||||
get builder => (context, controller) {
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// TODO: should we support multiple selection?
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
if (node == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final bgColor =
|
||||
node.attributes[blockComponentBackgroundColor] as String?;
|
||||
final selectedColor = convertHexToSelectOptionColorPB(
|
||||
bgColor,
|
||||
context,
|
||||
);
|
||||
|
||||
return SelectOptionColorList(
|
||||
selectedColor: selectedColor,
|
||||
onSelectedColor: (color) {
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final transaction = editorState.transaction;
|
||||
for (final node in nodes) {
|
||||
transaction.updateNode(node, {
|
||||
blockComponentBackgroundColor: color.make(context).toHex(),
|
||||
});
|
||||
}
|
||||
editorState.apply(transaction);
|
||||
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
SelectOptionColorPB? convertHexToSelectOptionColorPB(
|
||||
String? hexColor,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (hexColor == null) {
|
||||
return null;
|
||||
}
|
||||
for (final value in SelectOptionColorPB.values) {
|
||||
if (value.make(context).toHex() == hexColor) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class OptionActionWrapper extends ActionCell {
|
||||
final OptionAction inner;
|
||||
|
||||
OptionActionWrapper(this.inner);
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) {
|
||||
var name = '';
|
||||
// TODO: add icons.
|
||||
switch (inner) {
|
||||
case OptionAction.delete:
|
||||
name = 'editor/delete';
|
||||
break;
|
||||
case OptionAction.duplicate:
|
||||
name = 'editor/duplicate';
|
||||
break;
|
||||
case OptionAction.turnInto:
|
||||
name = 'editor/turn_into';
|
||||
break;
|
||||
case OptionAction.moveUp:
|
||||
name = 'editor/move_up';
|
||||
break;
|
||||
case OptionAction.moveDown:
|
||||
name = 'editor/move_down';
|
||||
break;
|
||||
case OptionAction.color:
|
||||
name = 'editor/color';
|
||||
break;
|
||||
case OptionAction.divider:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
if (name.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
name = 'editor/delete';
|
||||
return svgWidget(
|
||||
name,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get name {
|
||||
var description = '';
|
||||
switch (inner) {
|
||||
// TODO: l10n
|
||||
case OptionAction.delete:
|
||||
description = 'Delete';
|
||||
break;
|
||||
case OptionAction.duplicate:
|
||||
description = 'Duplicate';
|
||||
break;
|
||||
case OptionAction.turnInto:
|
||||
description = 'Turn into';
|
||||
break;
|
||||
case OptionAction.moveUp:
|
||||
description = 'Move up';
|
||||
break;
|
||||
case OptionAction.moveDown:
|
||||
description = 'Move down';
|
||||
break;
|
||||
case OptionAction.color:
|
||||
description = 'Color';
|
||||
break;
|
||||
case OptionAction.divider:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
return description;
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionActionList extends StatelessWidget {
|
||||
const OptionActionList({
|
||||
Key? key,
|
||||
required this.blockComponentContext,
|
||||
required this.blockComponentState,
|
||||
required this.actions,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final BlockComponentState blockComponentState;
|
||||
final List<OptionAction> actions;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final popoverActions = actions.map((e) {
|
||||
if (e == OptionAction.divider) {
|
||||
return DividerOptionAction();
|
||||
} else if (e == OptionAction.color) {
|
||||
return ColorOptionAction(
|
||||
editorState: editorState,
|
||||
);
|
||||
} else {
|
||||
return OptionActionWrapper(e);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return PopoverActionList<PopoverAction>(
|
||||
direction: PopoverDirection.leftWithCenterAligned,
|
||||
actions: popoverActions,
|
||||
onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
|
||||
onClosed: () {
|
||||
editorState.selectionType = null;
|
||||
editorState.selection = null;
|
||||
blockComponentState.alwaysShowActions = false;
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
if (action is OptionActionWrapper) {
|
||||
_onSelectAction(action.inner);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
buildChild: (controller) => OptionActionButton(
|
||||
onTap: () {
|
||||
controller.show();
|
||||
|
||||
// update selection
|
||||
_updateBlockSelection();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateBlockSelection() {
|
||||
final startNode = blockComponentContext.node;
|
||||
var endNode = startNode;
|
||||
while (endNode.children.isNotEmpty) {
|
||||
endNode = endNode.children.last;
|
||||
}
|
||||
|
||||
final start = Position(path: startNode.path, offset: 0);
|
||||
final end = endNode.selectable?.end() ??
|
||||
Position(
|
||||
path: endNode.path,
|
||||
offset: endNode.delta?.length ?? 0,
|
||||
);
|
||||
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = Selection(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectAction(OptionAction action) {
|
||||
final node = blockComponentContext.node;
|
||||
final transaction = editorState.transaction;
|
||||
switch (action) {
|
||||
case OptionAction.delete:
|
||||
transaction.deleteNode(node);
|
||||
break;
|
||||
case OptionAction.duplicate:
|
||||
transaction.insertNode(
|
||||
node.path.next,
|
||||
node.copyWith(),
|
||||
);
|
||||
break;
|
||||
case OptionAction.turnInto:
|
||||
break;
|
||||
case OptionAction.moveUp:
|
||||
transaction.moveNode(node.path.previous, node);
|
||||
break;
|
||||
case OptionAction.moveDown:
|
||||
transaction.moveNode(node.path.next.next, node);
|
||||
break;
|
||||
case OptionAction.color:
|
||||
// show the color picker
|
||||
|
||||
break;
|
||||
case OptionAction.divider:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
class BlockComponentActionButton extends StatelessWidget {
|
||||
const BlockComponentActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isHovering = false;
|
||||
final Widget icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onTapDown: (details) {},
|
||||
onTapUp: (details) {},
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OptionActionButton extends StatelessWidget {
|
||||
const OptionActionButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: IgnoreParentGestureWidget(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: svgWidget(
|
||||
'editor/option',
|
||||
size: const Size.square(24.0),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
extension BuildContextExtension on BuildContext {
|
||||
/// returns a boolean value indicating whether the given offset is contained within the bounds of the specified RenderBox or not.
|
||||
bool isOffsetInside(Offset offset) {
|
||||
final box = findRenderObject() as RenderBox?;
|
||||
if (box == null) {
|
||||
return false;
|
||||
}
|
||||
var result = BoxHitTestResult();
|
||||
box.hitTest(result, position: box.globalToLocal(offset));
|
||||
return result.path.any((entry) => entry.target == box);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@ -35,31 +35,17 @@ class BuiltInPageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
late Future<dartz.Either<FlowyError, ViewPB>> future;
|
||||
final focusNode = FocusNode();
|
||||
|
||||
String get gridID {
|
||||
return widget.node.attributes[kViewID];
|
||||
}
|
||||
|
||||
String get appID {
|
||||
return widget.node.attributes[kAppID];
|
||||
}
|
||||
String get appId => widget.node.attributes[DatabaseBlockKeys.kAppID];
|
||||
String get viewId => widget.node.attributes[DatabaseBlockKeys.kViewID];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final board = snapshot.data?.getLeftOrNull<ViewPB>();
|
||||
if (board != null) {
|
||||
return _build(context, board);
|
||||
}
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
future: AppBackendService().getChildView(appID, gridID),
|
||||
void initState() {
|
||||
super.initState();
|
||||
future = AppBackendService().getChildView(viewId, appId).then(
|
||||
(value) => value.swap(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,27 +55,44 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<FlowyError, ViewPB>>(
|
||||
builder: (context, snapshot) {
|
||||
final page = snapshot.data?.toOption().toNullable();
|
||||
if (snapshot.hasData && page != null) {
|
||||
return _build(context, page);
|
||||
}
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return const Center(
|
||||
child: FlowyText('Cannot load the page'),
|
||||
);
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
future: future,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context, ViewPB viewPB) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
widget.editorState.service.scrollService?.disable();
|
||||
},
|
||||
onExit: (event) {
|
||||
widget.editorState.service.scrollService?.enable();
|
||||
},
|
||||
onEnter: (_) => widget.editorState.service.scrollService?.disable(),
|
||||
onExit: (_) => widget.editorState.service.scrollService?.enable(),
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildMenu(context, viewPB),
|
||||
_buildGrid(context, viewPB),
|
||||
_buildPage(context, viewPB),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGrid(BuildContext context, ViewPB viewPB) {
|
||||
Widget _buildPage(BuildContext context, ViewPB viewPB) {
|
||||
return Focus(
|
||||
focusNode: focusNode,
|
||||
onFocusChange: (value) {
|
||||
@ -112,9 +115,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
// information
|
||||
FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_referencePage.tr(
|
||||
namedArgs: {
|
||||
'name': viewPB.layout.name,
|
||||
},
|
||||
namedArgs: {'name': viewPB.layout.name},
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
@ -137,8 +138,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
actions: _ActionType.values
|
||||
.map((action) => _ActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return FlowyIconButton(
|
||||
buildChild: (controller) => FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
|
||||
width: 24,
|
||||
height: 24,
|
||||
@ -148,8 +148,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
),
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case _ActionType.viewDatabase:
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,56 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiPickerButton extends StatelessWidget {
|
||||
EmojiPickerButton({
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.onSubmitted,
|
||||
this.emojiPickerSize = const Size(300, 250),
|
||||
this.emojiSize = 18.0,
|
||||
});
|
||||
|
||||
final String emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(Emoji emoji, PopoverController controller) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
triggerActions: PopoverTriggerFlags.click,
|
||||
constraints: BoxConstraints.expand(
|
||||
width: emojiPickerSize.width,
|
||||
height: emojiPickerSize.height,
|
||||
),
|
||||
popupBuilder: (context) => _buildEmojiPicker(),
|
||||
child: FlowyTextButton(
|
||||
emoji,
|
||||
fontSize: emojiSize,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 35.0),
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
popoverController.show();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiPicker() {
|
||||
return Container(
|
||||
width: emojiPickerSize.width,
|
||||
height: emojiPickerSize.height,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: EmojiSelectionMenu(
|
||||
onSubmitted: (emoji) => onSubmitted(emoji, popoverController),
|
||||
onExit: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,38 +1,41 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/board/board_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
const String kAppID = 'app_id';
|
||||
const String kViewID = 'view_id';
|
||||
class DatabaseBlockKeys {
|
||||
const DatabaseBlockKeys._();
|
||||
|
||||
extension InsertPage on EditorState {
|
||||
static const String kAppID = 'app_id';
|
||||
static const String kViewID = 'view_id';
|
||||
}
|
||||
|
||||
extension InsertDatabase on EditorState {
|
||||
Future<void> insertPage(ViewPB appPB, ViewPB viewPB) async {
|
||||
final selection = service.selectionService.currentSelection.value;
|
||||
final textNodes =
|
||||
service.selectionService.currentSelectedNodes.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
final selection = this.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final node = getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the database that the view is associated with
|
||||
final database =
|
||||
await DatabaseViewBackendService(viewId: viewPB.id).openGrid().then(
|
||||
(value) => value.getLeftOrNull(),
|
||||
);
|
||||
|
||||
final database = await DatabaseViewBackendService(viewId: viewPB.id)
|
||||
.openGrid()
|
||||
.then((value) => value.swap().toOption().toNullable());
|
||||
if (database == null) {
|
||||
throw StateError(
|
||||
'The database associated with ${viewPB.id} could not be found while attempting to create a referenced ${viewPB.layout.name}.',
|
||||
);
|
||||
}
|
||||
|
||||
final prefix = referencedBoardPrefix(viewPB.layout);
|
||||
|
||||
final prefix = _referencedDatabasePrefix(viewPB.layout);
|
||||
final ref = await AppBackendService().createView(
|
||||
appId: appPB.id,
|
||||
name: "$prefix ${viewPB.name}",
|
||||
@ -40,9 +43,7 @@ extension InsertPage on EditorState {
|
||||
ext: {
|
||||
'database_id': database.id,
|
||||
},
|
||||
).then(
|
||||
(value) => value.getLeftOrNull(),
|
||||
);
|
||||
).then((value) => value.swap().toOption().toNullable());
|
||||
|
||||
// TODO(a-wallen): Show error dialog here.
|
||||
if (ref == null) {
|
||||
@ -55,15 +56,15 @@ extension InsertPage on EditorState {
|
||||
Node(
|
||||
type: _convertPageType(viewPB),
|
||||
attributes: {
|
||||
kAppID: appPB.id,
|
||||
kViewID: ref.id,
|
||||
DatabaseBlockKeys.kAppID: appPB.id,
|
||||
DatabaseBlockKeys.kViewID: ref.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
apply(transaction);
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
String referencedBoardPrefix(ViewLayoutPB layout) {
|
||||
String _referencedDatabasePrefix(ViewLayoutPB layout) {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.grid_referencedGridPrefix.tr();
|
||||
@ -77,9 +78,9 @@ extension InsertPage on EditorState {
|
||||
String _convertPageType(ViewPB viewPB) {
|
||||
switch (viewPB.layout) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return kGridType;
|
||||
return GridBlockKeys.type;
|
||||
case ViewLayoutPB.Board:
|
||||
return kBoardType;
|
||||
return BoardBlockKeys.type;
|
||||
default:
|
||||
throw Exception('Unknown layout type');
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -9,70 +10,37 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'insert_page_command.dart';
|
||||
|
||||
EditorState? _editorState;
|
||||
OverlayEntry? _linkToPageMenu;
|
||||
|
||||
void showLinkToPageMenu(
|
||||
OverlayState container,
|
||||
EditorState editorState,
|
||||
SelectionMenuService menuService,
|
||||
BuildContext context,
|
||||
ViewLayoutPB pageType,
|
||||
) {
|
||||
final alignment = menuService.alignment;
|
||||
final offset = menuService.offset;
|
||||
menuService.dismiss();
|
||||
|
||||
_editorState = editorState;
|
||||
final alignment = menuService.alignment;
|
||||
final offset = menuService.offset;
|
||||
final top = alignment == Alignment.bottomLeft ? offset.dy : null;
|
||||
final bottom = alignment == Alignment.topLeft ? offset.dy : null;
|
||||
|
||||
String hintText = '';
|
||||
switch (pageType) {
|
||||
case ViewLayoutPB.Grid:
|
||||
hintText = LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
|
||||
break;
|
||||
case ViewLayoutPB.Board:
|
||||
hintText = LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown layout type');
|
||||
}
|
||||
|
||||
_linkToPageMenu?.remove();
|
||||
_linkToPageMenu = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
final linkToPageMenuEntry = FullScreenOverlayEntry(
|
||||
top: top,
|
||||
bottom: bottom,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
builder: (context) => Material(
|
||||
color: Colors.transparent,
|
||||
child: LinkToPageMenu(
|
||||
editorState: editorState,
|
||||
layoutType: pageType,
|
||||
hintText: hintText,
|
||||
hintText: pageType.toHintText(),
|
||||
onSelected: (appPB, viewPB) {
|
||||
editorState.insertPage(appPB, viewPB);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_linkToPageMenu!);
|
||||
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(dismissLinkToPageMenu);
|
||||
}
|
||||
|
||||
void dismissLinkToPageMenu() {
|
||||
_linkToPageMenu?.remove();
|
||||
_linkToPageMenu = null;
|
||||
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(dismissLinkToPageMenu);
|
||||
_editorState = null;
|
||||
).build();
|
||||
container.insert(linkToPageMenuEntry);
|
||||
}
|
||||
|
||||
class LinkToPageMenu extends StatefulWidget {
|
||||
@ -133,6 +101,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Focus(
|
||||
focusNode: _focusNode,
|
||||
onKey: _onKey,
|
||||
@ -140,7 +109,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
width: 300,
|
||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: style.selectionMenuBackgroundColor,
|
||||
color: theme.cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5,
|
||||
@ -150,7 +119,11 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
],
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: _buildListWidget(context, _selectedIndex, _availableLayout),
|
||||
child: _buildListWidget(
|
||||
context,
|
||||
_selectedIndex,
|
||||
_availableLayout,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -272,3 +245,18 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on ViewLayoutPB {
|
||||
String toHintText() {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
|
||||
|
||||
case ViewLayoutPB.Board:
|
||||
return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
|
||||
|
||||
default:
|
||||
throw Exception('Unknown layout type');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SelectableItemListMenu extends StatelessWidget {
|
||||
const SelectableItemListMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.selectedIndex,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<String> items;
|
||||
final int selectedIndex;
|
||||
final void Function(int) onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return SelectableItem(
|
||||
isSelected: index == selectedIndex,
|
||||
item: item,
|
||||
onTap: () => onSelected(index),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableItem extends StatelessWidget {
|
||||
const SelectableItem({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
required this.item,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final String item;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(item),
|
||||
rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SelectableSvgWidget extends StatelessWidget {
|
||||
const SelectableSvgWidget({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return svgWidget(
|
||||
name,
|
||||
size: const Size.square(18.0),
|
||||
color: isSelected
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onBackground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableIconWidget extends StatelessWidget {
|
||||
const SelectableIconWidget({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Icon(
|
||||
icon,
|
||||
size: 18.0,
|
||||
color: isSelected
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onBackground,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
extension Capitalize on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${substring(1)}";
|
||||
}
|
||||
}
|
@ -5,30 +5,35 @@ enum TextRobotInputType {
|
||||
word,
|
||||
}
|
||||
|
||||
extension TextRobot on EditorState {
|
||||
class TextRobot {
|
||||
const TextRobot({
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
Future<void> autoInsertText(
|
||||
String text, {
|
||||
TextRobotInputType inputType = TextRobotInputType.word,
|
||||
Duration delay = const Duration(milliseconds: 10),
|
||||
}) async {
|
||||
if (text == '\n') {
|
||||
await insertNewLineAtCurrentSelection();
|
||||
return;
|
||||
return editorState.insertNewLine();
|
||||
}
|
||||
final lines = text.split('\n');
|
||||
for (final line in lines) {
|
||||
if (line.isEmpty) {
|
||||
await insertNewLineAtCurrentSelection();
|
||||
await editorState.insertNewLine();
|
||||
continue;
|
||||
}
|
||||
switch (inputType) {
|
||||
case TextRobotInputType.character:
|
||||
final iterator = line.runes.iterator;
|
||||
while (iterator.moveNext()) {
|
||||
await insertTextAtCurrentSelection(
|
||||
await editorState.insertTextAtCurrentSelection(
|
||||
iterator.currentAsString,
|
||||
);
|
||||
await Future.delayed(delay, () {});
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
break;
|
||||
case TextRobotInputType.word:
|
||||
@ -36,17 +41,17 @@ extension TextRobot on EditorState {
|
||||
if (words.length == 1 ||
|
||||
(words.length == 2 &&
|
||||
(words.first.isEmpty || words.last.isEmpty))) {
|
||||
await insertTextAtCurrentSelection(
|
||||
await editorState.insertTextAtCurrentSelection(
|
||||
line,
|
||||
);
|
||||
} else {
|
||||
for (final word in words.map((e) => '$e ')) {
|
||||
await insertTextAtCurrentSelection(
|
||||
await editorState.insertTextAtCurrentSelection(
|
||||
word,
|
||||
);
|
||||
}
|
||||
}
|
||||
await Future.delayed(delay, () {});
|
||||
await Future.delayed(delay);
|
||||
break;
|
||||
}
|
||||
}
|
@ -1,29 +1,24 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
||||
name: LocaleKeys.document_plugins_referencedBoard.tr(),
|
||||
icon: (editorState, onSelected) {
|
||||
return svgWidget(
|
||||
'editor/board',
|
||||
size: const Size.square(18.0),
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
);
|
||||
},
|
||||
// TODO(a-wallen): Translate keywords
|
||||
icon: (editorState, onSelected) => SelectableSvgWidget(
|
||||
name: 'editor/board',
|
||||
isSelected: onSelected,
|
||||
),
|
||||
keywords: ['referenced', 'board', 'kanban'],
|
||||
handler: (editorState, menuService, context) {
|
||||
final container = Overlay.of(context);
|
||||
showLinkToPageMenu(
|
||||
container,
|
||||
editorState,
|
||||
menuService,
|
||||
context,
|
||||
ViewLayoutPB.Board,
|
||||
);
|
||||
},
|
@ -0,0 +1,76 @@
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class BoardBlockKeys {
|
||||
const BoardBlockKeys._();
|
||||
|
||||
static const String type = 'board';
|
||||
}
|
||||
|
||||
class BoardBlockComponentBuilder extends BlockComponentBuilder {
|
||||
BoardBlockComponentBuilder({
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return BoardBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) =>
|
||||
node.children.isEmpty &&
|
||||
node.attributes[DatabaseBlockKeys.kAppID] is String &&
|
||||
node.attributes[DatabaseBlockKeys.kViewID] is String;
|
||||
}
|
||||
|
||||
class BoardBlockComponentWidget extends StatefulWidget {
|
||||
const BoardBlockComponentWidget({
|
||||
super.key,
|
||||
required this.configuration,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
State<BoardBlockComponentWidget> createState() =>
|
||||
_BoardBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _BoardBlockComponentWidgetState extends State<BoardBlockComponentWidget>
|
||||
with BlockComponentConfigurable {
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editorState = Provider.of<EditorState>(context, listen: false);
|
||||
return BuiltInPageWidget(
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
builder: (viewPB) {
|
||||
return BoardPage(
|
||||
key: ValueKey(viewPB.id),
|
||||
view: viewPB,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,26 +1,19 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_service.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
|
||||
SelectionMenuItem(
|
||||
name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
|
||||
icon: (editorState, onSelected) {
|
||||
return svgWidget(
|
||||
'editor/board',
|
||||
size: const Size.square(18.0),
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
);
|
||||
},
|
||||
// TODO(a-wallen): Translate keywords.
|
||||
icon: (editorState, onSelected) => SelectableSvgWidget(
|
||||
name: 'editor/board',
|
||||
isSelected: onSelected,
|
||||
),
|
||||
keywords: ['board', 'kanban'],
|
||||
handler: (editorState, menuService, context) async {
|
||||
if (!documentBloc.view.hasParentViewId()) {
|
@ -0,0 +1,203 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../base/emoji_picker_button.dart';
|
||||
|
||||
// defining the keys of the callout block's attributes for easy access
|
||||
class CalloutBlockKeys {
|
||||
const CalloutBlockKeys._();
|
||||
|
||||
static const String type = 'callout';
|
||||
|
||||
/// The content of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String delta = 'delta';
|
||||
|
||||
/// The background color of a callout block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String backgroundColor = blockComponentBackgroundColor;
|
||||
|
||||
/// The emoji icon of a callout block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String icon = 'icon';
|
||||
}
|
||||
|
||||
// creating a new callout node
|
||||
Node calloutNode({
|
||||
Delta? delta,
|
||||
String emoji = '📌',
|
||||
String backgroundColor = '#F0F0F0',
|
||||
}) {
|
||||
final attributes = {
|
||||
CalloutBlockKeys.delta: (delta ?? Delta()).toJson(),
|
||||
CalloutBlockKeys.icon: emoji,
|
||||
CalloutBlockKeys.backgroundColor: backgroundColor,
|
||||
};
|
||||
return Node(
|
||||
type: CalloutBlockKeys.type,
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
// defining the callout block menu item for selection
|
||||
SelectionMenuItem calloutItem = SelectionMenuItem.node(
|
||||
name: 'Callout',
|
||||
iconData: Icons.note,
|
||||
keywords: ['callout'],
|
||||
nodeBuilder: (editorState) => calloutNode(),
|
||||
replace: (_, node) => node.delta?.isEmpty ?? false,
|
||||
updateSelection: (_, path, __, ___) {
|
||||
return Selection.single(path: [...path, 0], startOffset: 0);
|
||||
},
|
||||
);
|
||||
|
||||
// building the callout block widget
|
||||
class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
||||
CalloutBlockComponentBuilder({
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return CalloutBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
// validate the data of the node, if the result is false, the node will be rendered as a placeholder
|
||||
@override
|
||||
bool validate(Node node) =>
|
||||
node.delta != null &&
|
||||
node.children.isEmpty &&
|
||||
node.attributes[CalloutBlockKeys.icon] is String &&
|
||||
node.attributes[CalloutBlockKeys.backgroundColor] is String;
|
||||
}
|
||||
|
||||
// the main widget for rendering the callout block
|
||||
class CalloutBlockComponentWidget extends StatefulWidget {
|
||||
const CalloutBlockComponentWidget({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.configuration,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
State<CalloutBlockComponentWidget> createState() =>
|
||||
_CalloutBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _CalloutBlockComponentWidgetState
|
||||
extends State<CalloutBlockComponentWidget>
|
||||
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
// the key used to identify this component
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
|
||||
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
// get the background color of the note block from the node's attributes
|
||||
Color get backgroundColor {
|
||||
final colorString =
|
||||
node.attributes[CalloutBlockKeys.backgroundColor] as String?;
|
||||
if (colorString == null) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return colorString.toColor();
|
||||
}
|
||||
|
||||
// get the emoji of the note block from the node's attributes or default to '📌'
|
||||
String get emoji => node.attributes[CalloutBlockKeys.icon] ?? '📌';
|
||||
|
||||
// get access to the editor state via provider
|
||||
late final editorState = Provider.of<EditorState>(context, listen: false);
|
||||
|
||||
// build the callout block widget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: backgroundColor,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// the emoji picker button for the note
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: EmojiPickerButton(
|
||||
key: ValueKey(emoji), // force to refresh the popover state
|
||||
emoji: emoji,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji.emoji);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: buildCalloutBlockComponent(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// build the richtext child
|
||||
Widget buildCalloutBlockComponent(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: FlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// set the emoji of the note block
|
||||
Future<void> setEmoji(String emoji) async {
|
||||
final transaction = editorState.transaction
|
||||
..updateNode(node, {
|
||||
CalloutBlockKeys.icon: emoji,
|
||||
})
|
||||
..afterSelection = Selection.collapse(
|
||||
node.path,
|
||||
node.delta?.length ?? 0,
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart' as highlight;
|
||||
import 'package:highlight/languages/all.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CodeBlockKeys {
|
||||
const CodeBlockKeys._();
|
||||
|
||||
static const String type = 'code';
|
||||
|
||||
/// The content of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String delta = 'delta';
|
||||
|
||||
/// The language of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String language = 'language';
|
||||
}
|
||||
|
||||
Node codeBlockNode({
|
||||
Delta? delta,
|
||||
String? language,
|
||||
}) {
|
||||
final attributes = {
|
||||
CodeBlockKeys.delta: (delta ?? Delta()).toJson(),
|
||||
CodeBlockKeys.language: language,
|
||||
};
|
||||
return Node(
|
||||
type: CodeBlockKeys.type,
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
// defining the callout block menu item for selection
|
||||
SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
|
||||
name: 'Code Block',
|
||||
iconData: Icons.abc,
|
||||
keywords: ['code', 'codeblock'],
|
||||
nodeBuilder: (editorState) => codeBlockNode(),
|
||||
replace: (_, node) => node.delta?.isEmpty ?? false,
|
||||
);
|
||||
|
||||
class CodeBlockComponentBuilder extends BlockComponentBuilder {
|
||||
CodeBlockComponentBuilder({
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
this.padding = const EdgeInsets.all(0),
|
||||
});
|
||||
|
||||
@override
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return CodeBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
padding: padding,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) => node.delta != null;
|
||||
}
|
||||
|
||||
class CodeBlockComponentWidget extends StatefulWidget {
|
||||
const CodeBlockComponentWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
this.padding = const EdgeInsets.all(0),
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final BlockComponentConfiguration configuration;
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
State<CodeBlockComponentWidget> createState() =>
|
||||
_CodeBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>> get containerKey => node.key;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
final popoverController = PopoverController();
|
||||
|
||||
final supportedLanguages = [
|
||||
'Assembly',
|
||||
'Bash',
|
||||
'BASIC',
|
||||
'C',
|
||||
'C#',
|
||||
'C++',
|
||||
'Clojure',
|
||||
'CSS',
|
||||
'Dart',
|
||||
'Docker',
|
||||
'Elixir',
|
||||
'Elm',
|
||||
'Erlang',
|
||||
'Fortran',
|
||||
'Go',
|
||||
'GraphQL',
|
||||
'Haskell',
|
||||
'HTML',
|
||||
'Java',
|
||||
'JavaScript',
|
||||
'JSON',
|
||||
'Kotlin',
|
||||
'LaTeX',
|
||||
'Lisp',
|
||||
'Lua',
|
||||
'Markdown',
|
||||
'MATLAB',
|
||||
'Objective-C',
|
||||
'OCaml',
|
||||
'Perl',
|
||||
'PHP',
|
||||
'PowerShell',
|
||||
'Python',
|
||||
'R',
|
||||
'Ruby',
|
||||
'Rust',
|
||||
'Scala',
|
||||
'Shell',
|
||||
'SQL',
|
||||
'Swift',
|
||||
'TypeScript',
|
||||
'Visual Basic',
|
||||
'XML',
|
||||
'YAML',
|
||||
];
|
||||
late final languages = supportedLanguages
|
||||
.map((e) => e.toLowerCase())
|
||||
.toSet()
|
||||
.intersection(allLanguages.keys.toSet())
|
||||
.toList();
|
||||
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
String? get language => node.attributes[CodeBlockKeys.language] as String?;
|
||||
String? autoDetectLanguage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildSwitchLanguageButton(context),
|
||||
_buildCodeBlock(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeBlock(BuildContext context) {
|
||||
final delta = node.delta ?? Delta();
|
||||
final content = delta.toPlainText();
|
||||
|
||||
final result = highlight.highlight.parse(
|
||||
content,
|
||||
language: language,
|
||||
autoDetection: language == null,
|
||||
);
|
||||
autoDetectLanguage = language ?? result.language;
|
||||
|
||||
final codeNodes = result.nodes;
|
||||
if (codeNodes == null) {
|
||||
throw Exception('Code block parse error.');
|
||||
}
|
||||
final codeTextSpans = _convert(codeNodes);
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: FlowyRichText(
|
||||
key: forwardKey,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => TextSpan(
|
||||
style: textStyle,
|
||||
children: codeTextSpans,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => TextSpan(
|
||||
style: textStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchLanguageButton(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
child: Container(
|
||||
width: 100,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FlowyTextButton(
|
||||
'${language?.capitalize() ?? 'auto'} ',
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return SelectableItemListMenu(
|
||||
items: languages.map((e) => e.capitalize()).toList(),
|
||||
selectedIndex: languages.indexOf(language ?? ''),
|
||||
onSelected: (index) {
|
||||
updateLanguage(languages[index]);
|
||||
popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateLanguage(String language) async {
|
||||
final transaction = editorState.transaction
|
||||
..updateNode(node, {
|
||||
CodeBlockKeys.language: language,
|
||||
})
|
||||
..afterSelection = Selection.collapse(
|
||||
node.path,
|
||||
node.delta?.length ?? 0,
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
// Copy from flutter.highlight package.
|
||||
// https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
|
||||
List<TextSpan> _convert(List<highlight.Node> nodes) {
|
||||
List<TextSpan> spans = [];
|
||||
var currentSpans = spans;
|
||||
List<List<TextSpan>> stack = [];
|
||||
|
||||
void traverse(highlight.Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(
|
||||
node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(
|
||||
text: node.value,
|
||||
style: _builtInCodeBlockTheme[node.className!],
|
||||
),
|
||||
);
|
||||
} else if (node.children != null) {
|
||||
List<TextSpan> tmp = [];
|
||||
currentSpans.add(
|
||||
TextSpan(
|
||||
children: tmp,
|
||||
style: _builtInCodeBlockTheme[node.className!],
|
||||
),
|
||||
);
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (var n in node.children!) {
|
||||
traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var node in nodes) {
|
||||
traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
const _builtInCodeBlockTheme = {
|
||||
'root':
|
||||
TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
|
||||
'comment': TextStyle(color: Color(0xff007400)),
|
||||
'quote': TextStyle(color: Color(0xff007400)),
|
||||
'tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'attribute': TextStyle(color: Color(0xffaa0d91)),
|
||||
'keyword': TextStyle(color: Color(0xffaa0d91)),
|
||||
'selector-tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'literal': TextStyle(color: Color(0xffaa0d91)),
|
||||
'name': TextStyle(color: Color(0xffaa0d91)),
|
||||
'variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'template-variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'code': TextStyle(color: Color(0xffc41a16)),
|
||||
'string': TextStyle(color: Color(0xffc41a16)),
|
||||
'meta-string': TextStyle(color: Color(0xffc41a16)),
|
||||
'regexp': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'link': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'title': TextStyle(color: Color(0xff1c00cf)),
|
||||
'symbol': TextStyle(color: Color(0xff1c00cf)),
|
||||
'bullet': TextStyle(color: Color(0xff1c00cf)),
|
||||
'number': TextStyle(color: Color(0xff1c00cf)),
|
||||
'section': TextStyle(color: Color(0xff643820)),
|
||||
'meta': TextStyle(color: Color(0xff643820)),
|
||||
'type': TextStyle(color: Color(0xff5c2699)),
|
||||
'built_in': TextStyle(color: Color(0xff5c2699)),
|
||||
'builtin-name': TextStyle(color: Color(0xff5c2699)),
|
||||
'params': TextStyle(color: Color(0xff5c2699)),
|
||||
'attr': TextStyle(color: Color(0xff836C28)),
|
||||
'subst': TextStyle(color: Color(0xff000000)),
|
||||
'formula': TextStyle(
|
||||
backgroundColor: Color(0xffeeeeee),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
|
||||
'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
|
||||
'selector-id': TextStyle(color: Color(0xff9b703f)),
|
||||
'selector-class': TextStyle(color: Color(0xff9b703f)),
|
||||
'doctag': TextStyle(fontWeight: FontWeight.bold),
|
||||
'strong': TextStyle(fontWeight: FontWeight.bold),
|
||||
'emphasis': TextStyle(fontStyle: FontStyle.italic),
|
||||
};
|
@ -0,0 +1,229 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final List<CharacterShortcutEvent> codeBlockCharacterEvents = [
|
||||
enterInCodeBlock,
|
||||
...ignoreKeysInCodeBlock,
|
||||
];
|
||||
|
||||
final List<CommandShortcutEvent> codeBlockCommands = [
|
||||
insertNewParagraphNextToCodeBlockCommand,
|
||||
tabToInsertSpacesInCodeBlockCommand,
|
||||
tabToDeleteSpacesInCodeBlockCommand,
|
||||
];
|
||||
|
||||
/// press the enter key in code block to insert a new line in it.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final CharacterShortcutEvent enterInCodeBlock = CharacterShortcutEvent(
|
||||
key: 'press enter in code block',
|
||||
character: '\n',
|
||||
handler: _enterInCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// ignore ' ', '/', '_', '*' in code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final List<CharacterShortcutEvent> ignoreKeysInCodeBlock =
|
||||
[' ', '/', '_', '*', '~']
|
||||
.map(
|
||||
(e) => CharacterShortcutEvent(
|
||||
key: 'press enter in code block',
|
||||
character: e,
|
||||
handler: (editorState) => _ignoreKeysInCodeBlockCommandHandler(
|
||||
editorState,
|
||||
e,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// shift + enter to insert a new node next to the code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
///
|
||||
final CommandShortcutEvent insertNewParagraphNextToCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'insert a new paragraph next to the code block',
|
||||
command: 'shift+enter',
|
||||
handler: _insertNewParagraphNextToCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// tab to insert two spaces at the line start in code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent tabToInsertSpacesInCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'tab to insert two spaces at the line start in code block',
|
||||
command: 'tab',
|
||||
handler: _tabToInsertSpacesInCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// shift+tab to delete two spaces at the line start in code block if needed.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent tabToDeleteSpacesInCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'shift + tab to delete two spaces at the line start in code block',
|
||||
command: 'shift+tab',
|
||||
handler: _tabToDeleteSpacesInCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
CharacterShortcutEventHandler _enterInCodeBlockCommandHandler =
|
||||
(editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null || node.type != CodeBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
selection.end.offset,
|
||||
'\n',
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
return true;
|
||||
};
|
||||
|
||||
Future<bool> _ignoreKeysInCodeBlockCommandHandler(
|
||||
EditorState editorState,
|
||||
String key,
|
||||
) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null || node.type != CodeBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
await editorState.insertTextAtCurrentSelection(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
CommandShortcutEventHandler _insertNewParagraphNextToCodeBlockCommandHandler =
|
||||
(editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final sliced = delta.slice(selection.startIndex);
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(
|
||||
// delete the text after the cursor in the code block
|
||||
node,
|
||||
selection.startIndex,
|
||||
delta.length - selection.startIndex,
|
||||
)
|
||||
..insertNode(
|
||||
// insert a new paragraph node with the sliced delta after the code block
|
||||
selection.end.path.next,
|
||||
paragraphNode(
|
||||
attributes: {
|
||||
'delta': sliced.toJson(),
|
||||
},
|
||||
),
|
||||
)
|
||||
..afterSelection = Selection.collapse(
|
||||
selection.end.path.next,
|
||||
0,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _tabToInsertSpacesInCodeBlockCommandHandler =
|
||||
(editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
const spaces = ' ';
|
||||
final lines = delta.toPlainText().split('\n');
|
||||
var index = 0;
|
||||
for (final line in lines) {
|
||||
if (index <= selection.endIndex &&
|
||||
selection.endIndex <= index + line.length) {
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
index,
|
||||
spaces, // two spaces
|
||||
)
|
||||
..afterSelection = Selection.collapse(
|
||||
selection.end.path,
|
||||
selection.endIndex + spaces.length,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
break;
|
||||
}
|
||||
index += line.length + 1;
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _tabToDeleteSpacesInCodeBlockCommandHandler =
|
||||
(editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
const spaces = ' ';
|
||||
final lines = delta.toPlainText().split('\n');
|
||||
var index = 0;
|
||||
for (final line in lines) {
|
||||
if (index <= selection.endIndex &&
|
||||
selection.endIndex <= index + line.length) {
|
||||
if (line.startsWith(spaces)) {
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(
|
||||
node,
|
||||
index,
|
||||
spaces.length, // two spaces
|
||||
)
|
||||
..afterSelection = Selection.collapse(
|
||||
selection.end.path,
|
||||
selection.endIndex - spaces.length,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
break;
|
||||
}
|
||||
index += line.length + 1;
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -2,7 +2,7 @@ import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@ -55,7 +55,7 @@ class CoverColorPicker extends StatefulWidget {
|
||||
|
||||
final Color pickerBackgroundColor;
|
||||
final Color pickerItemHoverColor;
|
||||
final void Function(String color) onSubmittedbackgroundColorHex;
|
||||
final void Function(String color) onSubmittedBackgroundColorHex;
|
||||
final List<ColorOption> backgroundColorOptions;
|
||||
const CoverColorPicker({
|
||||
super.key,
|
||||
@ -63,7 +63,7 @@ class CoverColorPicker extends StatefulWidget {
|
||||
required this.pickerBackgroundColor,
|
||||
required this.backgroundColorOptions,
|
||||
required this.pickerItemHoverColor,
|
||||
required this.onSubmittedbackgroundColorHex,
|
||||
required this.onSubmittedBackgroundColorHex,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -215,21 +215,18 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
}
|
||||
|
||||
Widget _buildColorPickerList() {
|
||||
final theme = Theme.of(context);
|
||||
return CoverColorPicker(
|
||||
pickerBackgroundColor:
|
||||
widget.editorState.editorStyle.selectionMenuBackgroundColor ??
|
||||
Colors.white,
|
||||
pickerItemHoverColor:
|
||||
widget.editorState.editorStyle.selectionMenuItemSelectedColor ??
|
||||
Colors.blue.withOpacity(0.3),
|
||||
pickerBackgroundColor: theme.cardColor,
|
||||
pickerItemHoverColor: theme.hoverColor,
|
||||
selectedBackgroundColorHex:
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute] ==
|
||||
CoverSelectionType.color.toString()
|
||||
? widget.node.attributes[kCoverSelectionAttribute]
|
||||
: "ffffff",
|
||||
: 'ffffff',
|
||||
backgroundColorOptions:
|
||||
_generateBackgroundColorOptions(widget.editorState),
|
||||
onSubmittedbackgroundColorHex: (color) {
|
||||
onSubmittedBackgroundColorHex: (color) {
|
||||
widget.onCoverChanged(CoverSelectionType.color, color);
|
||||
setState(() {});
|
||||
},
|
||||
@ -497,7 +494,7 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
|
||||
),
|
||||
hoverColor: widget.pickerItemHoverColor,
|
||||
onTap: () {
|
||||
widget.onSubmittedbackgroundColorHex(option.colorHex);
|
||||
widget.onSubmittedBackgroundColorHex(option.colorHex);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
@ -544,9 +541,3 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Color {
|
||||
String toHex() {
|
||||
return '0x${value.toRadixString(16)}';
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@ -39,7 +40,7 @@ enum CoverSelectionType {
|
||||
class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _CoverImageNodeWidget(
|
||||
return CoverImageNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
@ -52,8 +53,8 @@ class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
class _CoverImageNodeWidget extends StatefulWidget {
|
||||
const _CoverImageNodeWidget({
|
||||
class CoverImageNodeWidget extends StatefulWidget {
|
||||
const CoverImageNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
@ -63,14 +64,32 @@ class _CoverImageNodeWidget extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
|
||||
State<CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
|
||||
}
|
||||
|
||||
class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
|
||||
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
|
||||
CoverSelectionType get selectionType => CoverSelectionType.fromString(
|
||||
widget.node.attributes[kCoverSelectionTypeAttribute],
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.node.addListener(_reload);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.node.removeListener(_reload);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reload() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
PopoverController iconPopoverController = PopoverController();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -141,7 +160,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
||||
height: widget.hasIcon ? 180 : 50.0,
|
||||
alignment: Alignment.bottomLeft,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 5),
|
||||
padding: EdgeInsets.only(
|
||||
left: EditorStyleCustomizer.horizontalPadding + 30,
|
||||
top: 20,
|
||||
bottom: 5,
|
||||
),
|
||||
child: isHidden
|
||||
? Container()
|
||||
: Row(
|
||||
@ -304,7 +327,8 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
),
|
||||
hasIcon
|
||||
? Positioned(
|
||||
bottom: !hasCover ? 30 : 10,
|
||||
left: EditorStyleCustomizer.horizontalPadding + 30,
|
||||
bottom: !hasCover ? 30 : 40,
|
||||
child: AppFlowyPopover(
|
||||
offset: const Offset(100, 0),
|
||||
controller: iconPopoverController,
|
||||
@ -313,9 +337,6 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
margin: EdgeInsets.zero,
|
||||
child: EmojiIconWidget(
|
||||
emoji: widget.node.attributes[kIconSelectionAttribute],
|
||||
onEmojiTapped: () {
|
||||
iconPopoverController.show();
|
||||
},
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return EmojiPopover(
|
||||
@ -454,7 +475,6 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
}
|
||||
|
||||
Widget _buildCoverImage(BuildContext context, EditorState editorState) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
const height = 250.0;
|
||||
final Widget coverImage;
|
||||
switch (selectionType) {
|
||||
@ -493,7 +513,7 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
coverImage = const SizedBox();
|
||||
break;
|
||||
}
|
||||
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
|
||||
// OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
setOverlayButtonsHidden(false);
|
||||
@ -503,8 +523,6 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
},
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: OverflowBox(
|
||||
maxWidth: screenSize.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
@ -519,7 +537,6 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiIconWidget extends StatefulWidget {
|
||||
final String? emoji;
|
||||
final void Function() onEmojiTapped;
|
||||
|
||||
const EmojiIconWidget({
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.onEmojiTapped,
|
||||
this.size = 80,
|
||||
this.emojiSize = 60,
|
||||
});
|
||||
|
||||
final String emoji;
|
||||
final double size;
|
||||
final double emojiSize;
|
||||
|
||||
@override
|
||||
State<EmojiIconWidget> createState() => _EmojiIconWidgetState();
|
||||
}
|
||||
@ -22,31 +23,22 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
setHidden(false);
|
||||
},
|
||||
onExit: (event) {
|
||||
setHidden(true);
|
||||
},
|
||||
onEnter: (_) => setHidden(false),
|
||||
onExit: (_) => setHidden(true),
|
||||
child: Container(
|
||||
height: 130,
|
||||
width: 130,
|
||||
margin: const EdgeInsets.only(top: 18),
|
||||
height: widget.size,
|
||||
width: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: !hover
|
||||
? Theme.of(context).colorScheme.inverseSurface
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s8Border,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FlowyText(
|
||||
widget.emoji.toString(),
|
||||
fontSize: 80,
|
||||
),
|
||||
],
|
||||
child: FlowyText(
|
||||
widget.emoji,
|
||||
fontSize: widget.emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
@ -0,0 +1,65 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/divider/divider_node_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// insert divider into a document by typing three minuses(-).
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final CharacterShortcutEvent convertMinusesToDivider = CharacterShortcutEvent(
|
||||
key: 'insert a divider',
|
||||
character: '-',
|
||||
handler: _convertMinusesToDividerHandler,
|
||||
);
|
||||
|
||||
CharacterShortcutEventHandler _convertMinusesToDividerHandler =
|
||||
(editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final path = selection.end.path;
|
||||
final node = editorState.getNodeAtPath(path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return false;
|
||||
}
|
||||
if (delta.toPlainText() != '--') {
|
||||
return false;
|
||||
}
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(path, dividerNode())
|
||||
..insertNode(path, paragraphNode())
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapse(path.next, 0);
|
||||
editorState.apply(transaction);
|
||||
return true;
|
||||
};
|
||||
|
||||
SelectionMenuItem dividerMenuItem = SelectionMenuItem(
|
||||
name: 'Divider',
|
||||
icon: (editorState, onSelected) => const Icon(
|
||||
Icons.horizontal_rule,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['horizontal rule', 'divider'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final path = selection.end.path;
|
||||
final node = editorState.getNodeAtPath(path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
final insertedPath = delta.isEmpty ? path : path.next;
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(insertedPath, dividerNode());
|
||||
editorState.apply(transaction);
|
||||
},
|
||||
);
|
@ -0,0 +1,107 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DividerBlockKeys {
|
||||
const DividerBlockKeys._();
|
||||
|
||||
static const String type = 'divider';
|
||||
}
|
||||
|
||||
// creating a new callout node
|
||||
Node dividerNode() {
|
||||
return Node(
|
||||
type: DividerBlockKeys.type,
|
||||
);
|
||||
}
|
||||
|
||||
class DividerBlockComponentBuilder extends BlockComponentBuilder {
|
||||
DividerBlockComponentBuilder({
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 8.0),
|
||||
this.lineColor = Colors.grey,
|
||||
});
|
||||
|
||||
final EdgeInsets padding;
|
||||
final Color lineColor;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return DividerBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
padding: padding,
|
||||
lineColor: lineColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) => node.children.isEmpty;
|
||||
}
|
||||
|
||||
class DividerBlockComponentWidget extends StatefulWidget {
|
||||
const DividerBlockComponentWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 8.0),
|
||||
this.lineColor = Colors.grey,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EdgeInsets padding;
|
||||
final Color lineColor;
|
||||
|
||||
@override
|
||||
State<DividerBlockComponentWidget> createState() =>
|
||||
_DividerBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _DividerBlockComponentWidgetState
|
||||
extends State<DividerBlockComponentWidget> with SelectableMixin {
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: widget.lineColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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.cover;
|
||||
|
||||
@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);
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
name: 'Emoji',
|
||||
icon: (editorState, onSelected) => SelectableIconWidget(
|
||||
icon: Icons.emoji_emotions_outlined,
|
||||
isSelected: onSelected,
|
||||
),
|
||||
keywords: ['emoji'],
|
||||
handler: (editorState, menuService, context) {
|
||||
final container = Overlay.of(context);
|
||||
showEmojiPickerMenu(
|
||||
container,
|
||||
editorState,
|
||||
menuService,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void showEmojiPickerMenu(
|
||||
OverlayState container,
|
||||
EditorState editorState,
|
||||
SelectionMenuService menuService,
|
||||
) {
|
||||
menuService.dismiss();
|
||||
|
||||
final alignment = menuService.alignment;
|
||||
final offset = menuService.offset;
|
||||
final top = alignment == Alignment.bottomLeft ? offset.dy : null;
|
||||
final bottom = alignment == Alignment.topLeft ? offset.dy : null;
|
||||
|
||||
final emojiPickerMenuEntry = FullScreenOverlayEntry(
|
||||
top: top,
|
||||
bottom: bottom,
|
||||
left: offset.dx,
|
||||
builder: (context) => Material(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 250,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: EmojiSelectionMenu(
|
||||
onSubmitted: (emoji) {
|
||||
editorState.insertTextAtCurrentSelection(emoji.emoji);
|
||||
},
|
||||
onExit: () {
|
||||
// close emoji panel
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
).build();
|
||||
container.insert(emojiPickerMenuEntry);
|
||||
}
|
||||
|
||||
class EmojiSelectionMenu extends StatefulWidget {
|
||||
const EmojiSelectionMenu({
|
||||
Key? key,
|
||||
required this.onSubmitted,
|
||||
required this.onExit,
|
||||
}) : super(key: key);
|
||||
|
||||
final void Function(Emoji emoji) onSubmitted;
|
||||
final void Function() onExit;
|
||||
|
||||
@override
|
||||
State<EmojiSelectionMenu> createState() => _EmojiSelectionMenuState();
|
||||
}
|
||||
|
||||
class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _handleGlobalKeyEvent(KeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape &&
|
||||
event is KeyDownEvent) {
|
||||
//triggers on esc
|
||||
widget.onExit();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
|
||||
config: const Config(
|
||||
columns: 7,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Colors.grey,
|
||||
iconColorSelected: Color(0xff333333),
|
||||
indicatorColor: Color(0xff333333),
|
||||
progressIndicatorColor: Color(0xff333333),
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,28 +1,24 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SelectionMenuItem gridMenuItem = SelectionMenuItem(
|
||||
name: LocaleKeys.document_plugins_referencedGrid.tr(),
|
||||
icon: (editorState, onSelected) {
|
||||
return svgWidget(
|
||||
'editor/grid',
|
||||
size: const Size.square(18.0),
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
);
|
||||
},
|
||||
icon: (editorState, onSelected) => SelectableSvgWidget(
|
||||
name: 'editor/board',
|
||||
isSelected: onSelected,
|
||||
),
|
||||
keywords: ['referenced', 'grid'],
|
||||
handler: (editorState, menuService, context) {
|
||||
final container = Overlay.of(context);
|
||||
showLinkToPageMenu(
|
||||
container,
|
||||
editorState,
|
||||
menuService,
|
||||
context,
|
||||
ViewLayoutPB.Grid,
|
||||
);
|
||||
},
|
@ -0,0 +1,76 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GridBlockKeys {
|
||||
const GridBlockKeys._();
|
||||
|
||||
static const String type = 'grid';
|
||||
}
|
||||
|
||||
class GridBlockComponentBuilder extends BlockComponentBuilder {
|
||||
GridBlockComponentBuilder({
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return GridBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) =>
|
||||
node.children.isEmpty &&
|
||||
node.attributes[DatabaseBlockKeys.kAppID] is String &&
|
||||
node.attributes[DatabaseBlockKeys.kViewID] is String;
|
||||
}
|
||||
|
||||
class GridBlockComponentWidget extends StatefulWidget {
|
||||
const GridBlockComponentWidget({
|
||||
super.key,
|
||||
required this.configuration,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
State<GridBlockComponentWidget> createState() =>
|
||||
_GridBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _GridBlockComponentWidgetState extends State<GridBlockComponentWidget>
|
||||
with BlockComponentConfigurable {
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editorState = Provider.of<EditorState>(context, listen: false);
|
||||
return BuiltInPageWidget(
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
builder: (viewPB) {
|
||||
return GridPage(
|
||||
key: ValueKey(viewPB.id),
|
||||
view: viewPB,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,25 +1,19 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
|
||||
SelectionMenuItem(
|
||||
name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
|
||||
icon: (editorState, onSelected) {
|
||||
return svgWidget(
|
||||
'editor/grid',
|
||||
size: const Size.square(18.0),
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
);
|
||||
},
|
||||
icon: (editorState, onSelected) => SelectableSvgWidget(
|
||||
name: 'editor/grid',
|
||||
isSelected: onSelected,
|
||||
),
|
||||
keywords: ['grid'],
|
||||
handler: (editorState, menuService, context) async {
|
||||
if (!documentBloc.view.hasParentViewId()) {
|
@ -0,0 +1,214 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_math_fork/flutter_math.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MathEquationBlockKeys {
|
||||
const MathEquationBlockKeys._();
|
||||
|
||||
static const String type = 'math_equation';
|
||||
|
||||
/// The content of a math equation block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String formula = 'formula';
|
||||
}
|
||||
|
||||
Node mathEquationNode({
|
||||
String formula = '',
|
||||
}) {
|
||||
final attributes = {
|
||||
MathEquationBlockKeys.formula: formula,
|
||||
};
|
||||
return Node(
|
||||
type: MathEquationBlockKeys.type,
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
// defining the callout block menu item for selection
|
||||
SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
|
||||
name: 'MathEquation',
|
||||
iconData: Icons.text_fields_rounded,
|
||||
keywords: ['tex, latex, katex', 'math equation', 'formula'],
|
||||
nodeBuilder: (editorState) => mathEquationNode(),
|
||||
replace: (_, node) => node.delta?.isEmpty ?? false,
|
||||
updateSelection: (editorState, path, __, ___) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final mathEquationState =
|
||||
editorState.getNodeAtPath(path)?.key.currentState;
|
||||
if (mathEquationState != null &&
|
||||
mathEquationState is _MathEquationBlockComponentWidgetState) {
|
||||
mathEquationState.showEditingDialog();
|
||||
}
|
||||
});
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
|
||||
MathEquationBlockComponentBuilder({
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return MathEquationBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) =>
|
||||
node.children.isEmpty &&
|
||||
node.attributes[MathEquationBlockKeys.formula] is String;
|
||||
}
|
||||
|
||||
class MathEquationBlockComponentWidget extends StatefulWidget {
|
||||
const MathEquationBlockComponentWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
this.configuration = const BlockComponentConfiguration(),
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final BlockComponentConfiguration configuration;
|
||||
|
||||
@override
|
||||
State<MathEquationBlockComponentWidget> createState() =>
|
||||
_MathEquationBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _MathEquationBlockComponentWidgetState
|
||||
extends State<MathEquationBlockComponentWidget>
|
||||
with BlockComponentConfigurable {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
bool isHover = false;
|
||||
String get formula =>
|
||||
widget.node.attributes[MathEquationBlockKeys.formula] as String;
|
||||
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onHover: (value) => setState(() => isHover = value),
|
||||
onTap: showEditingDialog,
|
||||
child: _buildMathEquation(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMathEquation(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: isHover || formula.isEmpty
|
||||
? Theme.of(context).colorScheme.tertiaryContainer
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: Center(
|
||||
child: formula.isEmpty
|
||||
? FlowyText.medium(
|
||||
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
|
||||
fontSize: 16,
|
||||
)
|
||||
: Math.tex(
|
||||
formula,
|
||||
textStyle: const TextStyle(fontSize: 20),
|
||||
mathStyle: MathStyle.display,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showEditingDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller = TextEditingController(text: formula);
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
title: Text(
|
||||
LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
|
||||
),
|
||||
content: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (key) {
|
||||
if (key is! RawKeyDownEvent) return;
|
||||
if (key.logicalKey == LogicalKeyboardKey.enter &&
|
||||
!key.isShiftPressed) {
|
||||
updateMathEquation(controller.text, context);
|
||||
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismiss(context);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.3,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
maxLines: null,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'E = MC^2',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () => dismiss(context),
|
||||
),
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_Done.tr(),
|
||||
onPressed: () => updateMathEquation(controller.text, context),
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.only(bottom: 20),
|
||||
actionsAlignment: MainAxisAlignment.spaceAround,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void updateMathEquation(String mathEquation, BuildContext context) {
|
||||
if (mathEquation == formula) {
|
||||
dismiss(context);
|
||||
return;
|
||||
}
|
||||
final transaction = editorState.transaction
|
||||
..updateNode(
|
||||
widget.node,
|
||||
{
|
||||
MathEquationBlockKeys.formula: mathEquation,
|
||||
},
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
dismiss(context);
|
||||
}
|
||||
|
||||
void dismiss(BuildContext context) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
|
||||
|
||||
import 'text_completion.dart';
|
||||
import 'package:dartz/dartz.dart';
|
@ -0,0 +1,418 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AutoCompletionBlockKeys {
|
||||
const AutoCompletionBlockKeys._();
|
||||
|
||||
static const String type = 'auto_completion';
|
||||
static const String prompt = 'prompt';
|
||||
static const String startSelection = 'start_selection';
|
||||
}
|
||||
|
||||
Node autoCompletionNode({
|
||||
String prompt = '',
|
||||
required Selection start,
|
||||
}) {
|
||||
return Node(
|
||||
type: AutoCompletionBlockKeys.type,
|
||||
attributes: {
|
||||
AutoCompletionBlockKeys.prompt: prompt,
|
||||
AutoCompletionBlockKeys.startSelection: start.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
|
||||
iconData: Icons.generating_tokens,
|
||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
||||
nodeBuilder: (editorState) {
|
||||
final node = autoCompletionNode(start: editorState.selection!);
|
||||
return node;
|
||||
},
|
||||
replace: (_, node) => false,
|
||||
);
|
||||
|
||||
class AutoCompletionBlockComponentBuilder extends BlockComponentBuilder {
|
||||
AutoCompletionBlockComponentBuilder();
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return AutoCompletionBlockComponent(
|
||||
key: node.key,
|
||||
node: node,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) {
|
||||
return node.children.isEmpty &&
|
||||
node.attributes[AutoCompletionBlockKeys.prompt] is String &&
|
||||
node.attributes[AutoCompletionBlockKeys.startSelection] is Map;
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCompletionBlockComponent extends StatefulWidget {
|
||||
const AutoCompletionBlockComponent({
|
||||
super.key,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
State<AutoCompletionBlockComponent> createState() =>
|
||||
_AutoCompletionBlockComponentState();
|
||||
}
|
||||
|
||||
class _AutoCompletionBlockComponentState
|
||||
extends State<AutoCompletionBlockComponent> {
|
||||
final controller = TextEditingController();
|
||||
final textFieldFocusNode = FocusNode();
|
||||
|
||||
late final editorState = context.read<EditorState>();
|
||||
late final SelectionGestureInterceptor interceptor;
|
||||
|
||||
String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_subscribeSelectionGesture();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
editorState.selection = null;
|
||||
textFieldFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unsubscribeSelectionGesture();
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 5,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const AutoCompletionHeader(),
|
||||
const Space(0, 10),
|
||||
if (prompt.isEmpty) ...[
|
||||
_buildInputWidget(context),
|
||||
const Space(0, 10),
|
||||
AutoCompletionInputFooter(
|
||||
onGenerate: _onGenerate,
|
||||
onExit: _onExit,
|
||||
),
|
||||
] else ...[
|
||||
AutoCompletionFooter(
|
||||
onKeep: _onExit,
|
||||
onDiscard: _onDiscard,
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputWidget(BuildContext context) {
|
||||
return FlowyTextField(
|
||||
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
focusNode: textFieldFocusNode,
|
||||
autoFocus: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onExit() async {
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
// disable undo/redo
|
||||
recordRedo: false,
|
||||
recordUndo: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onGenerate() async {
|
||||
final loading = Loading(context);
|
||||
loading.start();
|
||||
|
||||
await _updateEditingText();
|
||||
|
||||
final userProfile = await UserBackendService.getCurrentUserProfile()
|
||||
.then((value) => value.toOption().toNullable());
|
||||
if (userProfile == null) {
|
||||
loading.stop();
|
||||
await _showError(
|
||||
LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final textRobot = TextRobot(editorState: editorState);
|
||||
BarrierDialog? barrierDialog;
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: userProfile.openaiKey,
|
||||
);
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
prompt: controller.text,
|
||||
onStart: () async {
|
||||
loading.stop();
|
||||
barrierDialog = BarrierDialog(context);
|
||||
barrierDialog?.show();
|
||||
await _makeSurePreviousNodeIsEmptyParagraphNode();
|
||||
},
|
||||
onProcess: (response) async {
|
||||
if (response.choices.isNotEmpty) {
|
||||
final text = response.choices.first.text;
|
||||
await textRobot.autoInsertText(
|
||||
text,
|
||||
inputType: TextRobotInputType.word,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
}
|
||||
},
|
||||
onEnd: () async {
|
||||
await barrierDialog?.dismiss();
|
||||
},
|
||||
onError: (error) async {
|
||||
loading.stop();
|
||||
await _showError(error.message);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDiscard() async {
|
||||
final selection =
|
||||
widget.node.attributes[AutoCompletionBlockKeys.startSelection];
|
||||
if (selection != null) {
|
||||
final start = Selection.fromJson(selection).start.path;
|
||||
final end = widget.node.previous?.path;
|
||||
if (end != null) {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.deleteNodesAtPath(
|
||||
start,
|
||||
end.last - start.last + 1,
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
_onExit();
|
||||
}
|
||||
|
||||
Future<void> _updateEditingText() async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(
|
||||
widget.node,
|
||||
{
|
||||
AutoCompletionBlockKeys.prompt: controller.text,
|
||||
},
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _makeSurePreviousNodeIsEmptyParagraphNode() async {
|
||||
// make sure the previous node is a empty paragraph node without any styles.
|
||||
final transaction = editorState.transaction;
|
||||
final previous = widget.node.previous;
|
||||
final Selection selection;
|
||||
if (previous == null ||
|
||||
previous.type != 'paragraph' ||
|
||||
(previous.delta?.toPlainText().isNotEmpty ?? false)) {
|
||||
selection = Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
);
|
||||
transaction.insertNode(
|
||||
widget.node.path,
|
||||
paragraphNode(),
|
||||
);
|
||||
} else {
|
||||
selection = Selection.single(
|
||||
path: previous.path,
|
||||
startOffset: 0,
|
||||
);
|
||||
}
|
||||
transaction.updateNode(widget.node, {
|
||||
AutoCompletionBlockKeys.startSelection: selection.toJson(),
|
||||
});
|
||||
transaction.afterSelection = selection;
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _showError(String message) async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
action: SnackBarAction(
|
||||
label: LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
content: FlowyText(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _subscribeSelectionGesture() {
|
||||
interceptor = SelectionGestureInterceptor(
|
||||
key: AutoCompletionBlockKeys.type,
|
||||
canTap: (details) {
|
||||
if (!context.isOffsetInside(details.globalPosition)) {
|
||||
if (prompt.isNotEmpty || controller.text.isNotEmpty) {
|
||||
// show dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => _onDiscard(),
|
||||
onCancel: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (controller.text.isEmpty) {
|
||||
_onExit();
|
||||
}
|
||||
}
|
||||
editorState.service.keyboardService?.disable();
|
||||
return false;
|
||||
},
|
||||
);
|
||||
editorState.service.selectionService.registerGestureInterceptor(
|
||||
interceptor,
|
||||
);
|
||||
}
|
||||
|
||||
void _unsubscribeSelectionGesture() {
|
||||
editorState.service.selectionService.unregisterGestureInterceptor(
|
||||
AutoCompletionBlockKeys.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCompletionHeader extends StatelessWidget {
|
||||
const AutoCompletionHeader({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCompletionInputFooter extends StatelessWidget {
|
||||
const AutoCompletionInputFooter({
|
||||
super.key,
|
||||
required this.onGenerate,
|
||||
required this.onExit,
|
||||
});
|
||||
|
||||
final VoidCallback onGenerate;
|
||||
final VoidCallback onExit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_generate.tr(),
|
||||
onPressed: onGenerate,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: onExit,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCompletionFooter extends StatelessWidget {
|
||||
const AutoCompletionFooter({
|
||||
super.key,
|
||||
required this.onKeep,
|
||||
required this.onDiscard,
|
||||
});
|
||||
|
||||
final VoidCallback onKeep;
|
||||
final VoidCallback onDiscard;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_keep.tr(),
|
||||
onPressed: onKeep,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_discard.tr(),
|
||||
onPressed: onDiscard,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Loading {
|
||||
Loading(this.context);
|
||||
|
||||
late BuildContext loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> start() async => await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SimpleDialog(
|
||||
elevation: 0.0,
|
||||
backgroundColor:
|
||||
Colors.transparent, // can change this to your preferred color
|
||||
children: [
|
||||
Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> stop() async => Navigator.of(loadingContext).pop();
|
||||
}
|
||||
|
||||
class BarrierDialog {
|
||||
BarrierDialog(this.context);
|
||||
|
||||
late BuildContext loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> show() async => await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> dismiss() async => Navigator.of(loadingContext).pop();
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@ -13,59 +13,98 @@ import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const String kSmartEditType = 'smart_edit_input';
|
||||
const String kSmartEditInstructionType = 'smart_edit_instruction';
|
||||
const String kSmartEditInputType = 'smart_edit_input';
|
||||
|
||||
class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return SmartEditAction.values
|
||||
.map((e) => e.index)
|
||||
.contains(node.attributes[kSmartEditInstructionType]) &&
|
||||
node.attributes[kSmartEditInputType] is String;
|
||||
};
|
||||
class SmartEditBlockKeys {
|
||||
const SmartEditBlockKeys._();
|
||||
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _HoverSmartInput(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
static const type = 'smart_edit';
|
||||
|
||||
/// The instruction of the smart edit.
|
||||
///
|
||||
/// It is a [SmartEditAction] value.
|
||||
static const action = 'action';
|
||||
|
||||
/// The input of the smart edit.
|
||||
static const content = 'content';
|
||||
}
|
||||
|
||||
class _HoverSmartInput extends StatefulWidget {
|
||||
const _HoverSmartInput({
|
||||
Node smartEditNode({
|
||||
required SmartEditAction action,
|
||||
required String content,
|
||||
}) {
|
||||
return Node(
|
||||
type: SmartEditBlockKeys.type,
|
||||
attributes: {
|
||||
SmartEditBlockKeys.action: action.index,
|
||||
SmartEditBlockKeys.content: content,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class SmartEditBlockComponentBuilder extends BlockComponentBuilder {
|
||||
SmartEditBlockComponentBuilder();
|
||||
|
||||
@override
|
||||
Widget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return SmartEditBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) =>
|
||||
node.attributes[SmartEditBlockKeys.action] is int &&
|
||||
node.attributes[SmartEditBlockKeys.content] is String;
|
||||
}
|
||||
|
||||
class SmartEditBlockComponentWidget extends StatefulWidget {
|
||||
const SmartEditBlockComponentWidget({
|
||||
required super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_HoverSmartInput> createState() => _HoverSmartInputState();
|
||||
State<SmartEditBlockComponentWidget> createState() =>
|
||||
_SmartEditBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _HoverSmartInputState extends State<_HoverSmartInput> {
|
||||
class _SmartEditBlockComponentWidgetState
|
||||
extends State<SmartEditBlockComponentWidget> {
|
||||
final popoverController = PopoverController();
|
||||
final key = GlobalKey(debugLabel: 'smart_edit_input');
|
||||
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// todo: don't use a popover to show the content of the smart edit.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
popoverController.show();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = _maxWidth();
|
||||
final width = _getEditorWidth();
|
||||
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
@ -82,7 +121,7 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
|
||||
),
|
||||
canClose: () async {
|
||||
final completer = Completer<bool>();
|
||||
final state = key.currentState as _SmartEditInputState;
|
||||
final state = key.currentState as _SmartEditInputWidgetState;
|
||||
if (state.result.isEmpty) {
|
||||
completer.complete(true);
|
||||
} else {
|
||||
@ -98,20 +137,24 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
|
||||
}
|
||||
return completer.future;
|
||||
},
|
||||
onClose: () {
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
editorState.apply(transaction);
|
||||
},
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return _SmartEditInput(
|
||||
return SmartEditInputWidget(
|
||||
key: key,
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
editorState: editorState,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _maxWidth() {
|
||||
double _getEditorWidth() {
|
||||
var width = double.infinity;
|
||||
final editorSize = widget.editorState.renderBox?.size;
|
||||
final padding = widget.editorState.editorStyle.padding;
|
||||
final editorSize = editorState.renderBox?.size;
|
||||
final padding = editorState.editorStyle.padding;
|
||||
if (editorSize != null && padding != null) {
|
||||
width = editorSize.width - padding.left - padding.right;
|
||||
}
|
||||
@ -119,8 +162,8 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SmartEditInput extends StatefulWidget {
|
||||
const _SmartEditInput({
|
||||
class SmartEditInputWidget extends StatefulWidget {
|
||||
const SmartEditInputWidget({
|
||||
required super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
@ -130,16 +173,19 @@ class _SmartEditInput extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_SmartEditInput> createState() => _SmartEditInputState();
|
||||
State<SmartEditInputWidget> createState() => _SmartEditInputWidgetState();
|
||||
}
|
||||
|
||||
class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
SmartEditAction get action =>
|
||||
SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
|
||||
String get input => widget.node.attributes[kSmartEditInputType];
|
||||
|
||||
class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
|
||||
final focusNode = FocusNode();
|
||||
final client = http.Client();
|
||||
|
||||
SmartEditAction get action => SmartEditAction.from(
|
||||
widget.node.attributes[SmartEditBlockKeys.action],
|
||||
);
|
||||
String get content => widget.node.attributes[SmartEditBlockKeys.content];
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
bool loading = true;
|
||||
String result = '';
|
||||
|
||||
@ -147,19 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.editorState.service.keyboardService?.disable(showCursor: true);
|
||||
focusNode.requestFocus();
|
||||
focusNode.addListener(() {
|
||||
if (!focusNode.hasFocus) {
|
||||
widget.editorState.service.keyboardService?.enable();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
editorState.service.keyboardService?.disable();
|
||||
// editorState.selection = null;
|
||||
});
|
||||
|
||||
focusNode.requestFocus();
|
||||
_requestCompletions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
client.close();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -272,12 +318,15 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
),
|
||||
onPressed: () async => await _onExit(),
|
||||
),
|
||||
const Spacer(flex: 2),
|
||||
const Spacer(flex: 1),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -285,72 +334,66 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
}
|
||||
|
||||
Future<void> _onReplace() async {
|
||||
final selection = widget.editorState.service.selectionService
|
||||
.currentSelection.value?.normalized;
|
||||
final selectedNodes = widget
|
||||
.editorState.service.selectionService.currentSelectedNodes.normalized
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || result.isEmpty) {
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
|
||||
final transaction = widget.editorState.transaction;
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
if (nodes.isEmpty || !nodes.every((element) => element.delta != null)) {
|
||||
return;
|
||||
}
|
||||
final replaceTexts = result.split('\n')
|
||||
..removeWhere((element) => element.isEmpty);
|
||||
final transaction = editorState.transaction;
|
||||
transaction.replaceTexts(
|
||||
selectedNodes.toList(growable: false),
|
||||
nodes,
|
||||
selection,
|
||||
texts,
|
||||
replaceTexts,
|
||||
);
|
||||
await widget.editorState.apply(transaction);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
int endOffset = texts.last.length;
|
||||
if (texts.length == 1) {
|
||||
int endOffset = replaceTexts.last.length;
|
||||
if (replaceTexts.length == 1) {
|
||||
endOffset += selection.start.offset;
|
||||
}
|
||||
|
||||
await widget.editorState.updateCursorSelection(
|
||||
Selection(
|
||||
editorState.selection = Selection(
|
||||
start: selection.start,
|
||||
end: Position(
|
||||
path: [selection.start.path.first + texts.length - 1],
|
||||
path: [selection.start.path.first + replaceTexts.length - 1],
|
||||
offset: endOffset,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onInsertBelow() async {
|
||||
final selection = widget.editorState.service.selectionService
|
||||
.currentSelection.value?.normalized;
|
||||
if (selection == null || result.isEmpty) {
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
|
||||
final transaction = widget.editorState.transaction;
|
||||
final insertedText = result.split('\n')
|
||||
..removeWhere((element) => element.isEmpty);
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
selection.normalized.end.path.next,
|
||||
texts.map(
|
||||
(e) => TextNode(
|
||||
delta: Delta()..insert(e),
|
||||
selection.end.path.next,
|
||||
insertedText.map(
|
||||
(e) => paragraphNode(
|
||||
text: e,
|
||||
),
|
||||
),
|
||||
);
|
||||
await widget.editorState.apply(transaction);
|
||||
|
||||
await widget.editorState.updateCursorSelection(
|
||||
Selection(
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: selection.end.path.next, offset: 0),
|
||||
end: Position(
|
||||
path: [selection.end.path.next.first + texts.length],
|
||||
),
|
||||
path: [selection.end.path.next.first + insertedText.length],
|
||||
),
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _onExit() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNode(widget.node);
|
||||
return widget.editorState.apply(
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
return editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
recordRedo: false,
|
||||
@ -362,7 +405,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
Future<void> _requestCompletions() async {
|
||||
final openAIRepository = await getIt.getAsync<OpenAIRepository>();
|
||||
|
||||
var lines = input.split('\n\n');
|
||||
var lines = content.split('\n\n');
|
||||
if (action == SmartEditAction.summarize) {
|
||||
lines = [lines.join('\n')];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -10,33 +10,34 @@ import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
ToolbarItem smartEditItem = ToolbarItem(
|
||||
id: 'appflowy.toolbar.smart_edit',
|
||||
type: 0, // headmost
|
||||
validator: (editorState) {
|
||||
// All selected nodes must be text.
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
return nodes.whereType<TextNode>().length == nodes.length;
|
||||
final ToolbarItem smartEditItem = ToolbarItem(
|
||||
id: 'appflowy.editor.smart_edit',
|
||||
isActive: (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
return nodes.every((element) => element.delta != null);
|
||||
},
|
||||
itemBuilder: (context, editorState) {
|
||||
return _SmartEditWidget(
|
||||
builder: (context, editorState) => SmartEditActionList(
|
||||
editorState: editorState,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
class _SmartEditWidget extends StatefulWidget {
|
||||
const _SmartEditWidget({
|
||||
class SmartEditActionList extends StatefulWidget {
|
||||
const SmartEditActionList({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_SmartEditWidget> createState() => _SmartEditWidgetState();
|
||||
State<SmartEditActionList> createState() => _SmartEditActionListState();
|
||||
}
|
||||
|
||||
class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
bool isOpenAIEnabled = false;
|
||||
|
||||
@override
|
||||
@ -45,8 +46,10 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
|
||||
UserBackendService.getCurrentUserProfile().then((value) {
|
||||
setState(() {
|
||||
isOpenAIEnabled =
|
||||
value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
|
||||
isOpenAIEnabled = value.fold(
|
||||
(l) => false,
|
||||
(r) => r.openaiKey.isNotEmpty,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -67,7 +70,7 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
preferBelow: false,
|
||||
icon: const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
size: 13,
|
||||
size: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
@ -89,40 +92,29 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
Future<void> _insertSmartEditNode(
|
||||
SmartEditActionWrapper actionWrapper,
|
||||
) async {
|
||||
final selection =
|
||||
widget.editorState.service.selectionService.currentSelection.value;
|
||||
final selection = widget.editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
final textNodes = widget
|
||||
.editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final input = widget.editorState.getTextInSelection(
|
||||
textNodes.normalized,
|
||||
selection.normalized,
|
||||
);
|
||||
final input = widget.editorState.getTextInSelection(selection);
|
||||
while (input.last.isEmpty) {
|
||||
input.removeLast();
|
||||
}
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.insertNode(
|
||||
selection.normalized.end.path.next,
|
||||
Node(
|
||||
type: kSmartEditType,
|
||||
attributes: {
|
||||
kSmartEditInstructionType: actionWrapper.inner.index,
|
||||
kSmartEditInputType: input.join('\n\n'),
|
||||
},
|
||||
smartEditNode(
|
||||
action: actionWrapper.inner,
|
||||
content: input.join('\n\n'),
|
||||
),
|
||||
);
|
||||
return widget.editorState.apply(
|
||||
await widget.editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
recordUndo: false,
|
||||
recordRedo: false,
|
||||
),
|
||||
withUpdateCursor: false,
|
||||
withUpdateSelection: false,
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class MathEquationNodeParser extends NodeParser {
|
||||
const MathEquationNodeParser();
|
||||
|
||||
@override
|
||||
String get id => 'math_equation';
|
||||
String get id => MathEquationBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node) {
|
@ -1,21 +1,20 @@
|
||||
export 'board/board_node_widget.dart';
|
||||
export 'board/board_menu_item.dart';
|
||||
export 'board/board_view_menu_item.dart';
|
||||
export 'callout/callout_node_widget.dart';
|
||||
export 'code_block/code_block_node_widget.dart';
|
||||
export 'callout/callout_block_component.dart';
|
||||
export 'code_block/code_block_component.dart';
|
||||
export 'code_block/code_block_shortcut_event.dart';
|
||||
export 'cover/change_cover_popover_bloc.dart';
|
||||
export 'cover/cover_node_widget.dart';
|
||||
export 'cover/cover_image_picker.dart';
|
||||
export 'divider/divider_node_widget.dart';
|
||||
export 'divider/divider_shortcut_event.dart';
|
||||
export 'divider/divider_character_shortcut_event.dart';
|
||||
export 'emoji_picker/emoji_menu_item.dart';
|
||||
export 'extensions/flowy_tint_extension.dart';
|
||||
export 'grid/grid_menu_item.dart';
|
||||
export 'grid/grid_node_widget.dart';
|
||||
export 'grid/grid_view_menu_item.dart';
|
||||
export 'math_equation/math_equation_node_widget.dart';
|
||||
export 'math_equation/math_equation_block_component.dart';
|
||||
export 'openai/widgets/auto_completion_node_widget.dart';
|
||||
export 'openai/widgets/auto_completion_plugins.dart';
|
||||
export 'openai/widgets/smart_edit_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
@ -0,0 +1,125 @@
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class EditorStyleCustomizer {
|
||||
EditorStyleCustomizer({
|
||||
required this.context,
|
||||
});
|
||||
|
||||
static double get horizontalPadding =>
|
||||
PlatformExtension.isDesktop ? 100.0 : 10.0;
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
EditorStyle style() {
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
return desktop();
|
||||
} else if (PlatformExtension.isMobile) {
|
||||
return mobile();
|
||||
}
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
EditorStyle desktop() {
|
||||
final theme = Theme.of(context);
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
return EditorStyle.desktop(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
cursorColor: theme.colorScheme.primary,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
text: TextStyle(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: fontSize,
|
||||
color: theme.colorScheme.onBackground,
|
||||
height: 1.5,
|
||||
),
|
||||
bold: const TextStyle(
|
||||
fontFamily: 'poppins-Bold',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
||||
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||
href: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
code: GoogleFonts.robotoMono(
|
||||
textStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.red,
|
||||
backgroundColor: theme.colorScheme.inverseSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
EditorStyle mobile() {
|
||||
final theme = Theme.of(context);
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
return EditorStyle.desktop(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
cursorColor: theme.colorScheme.primary,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
text: TextStyle(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: fontSize,
|
||||
color: theme.colorScheme.onBackground,
|
||||
height: 1.5,
|
||||
),
|
||||
bold: const TextStyle(
|
||||
fontFamily: 'poppins-Bold',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
||||
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||
href: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
code: GoogleFonts.robotoMono(
|
||||
textStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.red,
|
||||
backgroundColor: theme.colorScheme.inverseSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle headingStyleBuilder(int level) {
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
final fontSizes = [
|
||||
fontSize + 16,
|
||||
fontSize + 12,
|
||||
fontSize + 8,
|
||||
fontSize + 4,
|
||||
fontSize + 2,
|
||||
fontSize
|
||||
];
|
||||
return TextStyle(
|
||||
fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle codeBlockStyleBuilder() {
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
return TextStyle(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: fontSize,
|
||||
height: 1.5,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class ExportPageWidget extends StatelessWidget {
|
||||
const ExportPageWidget({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowyText.regular(
|
||||
'There are some errors.',
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const FlowyText.regular(
|
||||
'Please try to export the page and contact us.',
|
||||
fontSize: 14.0,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
FlowyTextButton(
|
||||
'Export page',
|
||||
constraints: const BoxConstraints(maxWidth: 100),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onPressed: onTap,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -20,11 +20,11 @@ class DocumentAppearance {
|
||||
}
|
||||
|
||||
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 14.0));
|
||||
DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 16.0));
|
||||
|
||||
void fetch() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 14.0;
|
||||
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
|
||||
emit(
|
||||
state.copyWith(
|
||||
fontSize: fontSize,
|
||||
|
@ -18,9 +18,9 @@ class FontSizeSwitcher extends StatefulWidget {
|
||||
|
||||
class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
||||
final List<Tuple3<String, double, bool>> _fontSizes = [
|
||||
Tuple3(LocaleKeys.moreAction_small.tr(), 12.0, false),
|
||||
Tuple3(LocaleKeys.moreAction_medium.tr(), 14.0, true),
|
||||
Tuple3(LocaleKeys.moreAction_large.tr(), 18.0, false),
|
||||
Tuple3(LocaleKeys.moreAction_small.tr(), 14.0, false),
|
||||
Tuple3(LocaleKeys.moreAction_medium.tr(), 18.0, true),
|
||||
Tuple3(LocaleKeys.moreAction_large.tr(), 22.0, false),
|
||||
];
|
||||
|
||||
@override
|
||||
|
@ -1,54 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String kBoardType = 'board';
|
||||
|
||||
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _BoardWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node.attributes[kViewID] is String &&
|
||||
node.attributes[kAppID] is String;
|
||||
};
|
||||
}
|
||||
|
||||
class _BoardWidget extends StatefulWidget {
|
||||
const _BoardWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_BoardWidget> createState() => _BoardWidgetState();
|
||||
}
|
||||
|
||||
class _BoardWidgetState extends State<_BoardWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuiltInPageWidget(
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
builder: (viewPB) {
|
||||
return BoardPage(
|
||||
key: ValueKey(viewPB.id),
|
||||
view: viewPB,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,300 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const String kCalloutType = 'callout';
|
||||
const String kCalloutAttrColor = 'color';
|
||||
const String kCalloutAttrEmoji = 'emoji';
|
||||
|
||||
SelectionMenuItem calloutMenuItem = SelectionMenuItem.node(
|
||||
name: 'Callout',
|
||||
iconData: Icons.note,
|
||||
keywords: ['callout'],
|
||||
nodeBuilder: (editorState) {
|
||||
final node = Node(type: kCalloutType);
|
||||
node.insert(TextNode.empty());
|
||||
return node;
|
||||
},
|
||||
replace: (_, textNode) => textNode.toPlainText().isEmpty,
|
||||
updateSelection: (_, path, __, ___) {
|
||||
return Selection.single(path: [...path, 0], startOffset: 0);
|
||||
},
|
||||
);
|
||||
|
||||
class CalloutNodeWidgetBuilder extends NodeWidgetBuilder<Node>
|
||||
with ActionProvider<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _CalloutWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) => node.type == kCalloutType;
|
||||
|
||||
_CalloutWidgetState? _getState(NodeWidgetContext<Node> context) {
|
||||
return context.node.key.currentState as _CalloutWidgetState?;
|
||||
}
|
||||
|
||||
BuildContext? _getBuildContext(NodeWidgetContext<Node> context) {
|
||||
return context.node.key.currentContext;
|
||||
}
|
||||
|
||||
@override
|
||||
List<ActionMenuItem> actions(NodeWidgetContext<Node> context) {
|
||||
return [
|
||||
ActionMenuItem.icon(
|
||||
iconData: Icons.color_lens_outlined,
|
||||
onPressed: () {
|
||||
final state = _getState(context);
|
||||
final ctx = _getBuildContext(context);
|
||||
if (state == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
final menuState = Provider.of<ActionMenuState>(ctx, listen: false);
|
||||
menuState.isPinned = true;
|
||||
state.colorPopoverController.show();
|
||||
},
|
||||
itemWrapper: (item) {
|
||||
final state = _getState(context);
|
||||
final ctx = _getBuildContext(context);
|
||||
if (state == null || ctx == null) {
|
||||
return item;
|
||||
}
|
||||
return AppFlowyPopover(
|
||||
controller: state.colorPopoverController,
|
||||
popupBuilder: (context) => state._buildColorPicker(),
|
||||
constraints: BoxConstraints.loose(const Size(200, 460)),
|
||||
triggerActions: 0,
|
||||
offset: const Offset(0, 30),
|
||||
child: item,
|
||||
onClose: () {
|
||||
final menuState =
|
||||
Provider.of<ActionMenuState>(ctx, listen: false);
|
||||
menuState.isPinned = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ActionMenuItem.svg(
|
||||
name: 'delete',
|
||||
onPressed: () {
|
||||
final transaction = context.editorState.transaction
|
||||
..deleteNode(context.node);
|
||||
context.editorState.apply(transaction);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutWidget extends StatefulWidget {
|
||||
const _CalloutWidget({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_CalloutWidget> createState() => _CalloutWidgetState();
|
||||
}
|
||||
|
||||
class _CalloutWidgetState extends State<_CalloutWidget> with SelectableMixin {
|
||||
final PopoverController colorPopoverController = PopoverController();
|
||||
final PopoverController emojiPopoverController = PopoverController();
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.node.addListener(nodeChanged);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.node.removeListener(nodeChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void nodeChanged() {
|
||||
if (widget.node.children.isEmpty) {
|
||||
deleteNode();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: tint.color(context),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 15),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildEmoji(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.node.children
|
||||
.map(
|
||||
(child) => widget.editorState.service.renderPluginService
|
||||
.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _popover({
|
||||
required PopoverController controller,
|
||||
required Widget Function(BuildContext context) popupBuilder,
|
||||
required Widget child,
|
||||
Size size = const Size(200, 460),
|
||||
}) {
|
||||
return AppFlowyPopover(
|
||||
controller: controller,
|
||||
constraints: BoxConstraints.loose(size),
|
||||
triggerActions: 0,
|
||||
popupBuilder: popupBuilder,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorPicker() {
|
||||
return FlowyColorPicker(
|
||||
colors: FlowyTint.values
|
||||
.map(
|
||||
(t) => ColorOption(
|
||||
color: t.color(context),
|
||||
name: t.tintName(AppFlowyEditorLocalizations.current),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
selected: tint.color(context),
|
||||
onTap: (color, index) {
|
||||
setColor(FlowyTint.values[index]);
|
||||
colorPopoverController.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmoji() {
|
||||
return _popover(
|
||||
controller: emojiPopoverController,
|
||||
popupBuilder: (context) => _buildEmojiPicker(),
|
||||
size: const Size(300, 200),
|
||||
child: FlowyTextButton(
|
||||
emoji,
|
||||
fontSize: 18,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
emojiPopoverController.show();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiPicker() {
|
||||
return EmojiSelectionMenu(
|
||||
editorState: widget.editorState,
|
||||
onSubmitted: (emoji) {
|
||||
setEmoji(emoji.emoji);
|
||||
emojiPopoverController.close();
|
||||
},
|
||||
onExit: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void setColor(FlowyTint tint) {
|
||||
final transaction = widget.editorState.transaction
|
||||
..updateNode(widget.node, {
|
||||
kCalloutAttrColor: tint.name,
|
||||
});
|
||||
widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
void setEmoji(String emoji) {
|
||||
final transaction = widget.editorState.transaction
|
||||
..updateNode(widget.node, {
|
||||
kCalloutAttrEmoji: emoji,
|
||||
});
|
||||
widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
void deleteNode() {
|
||||
final transaction = widget.editorState.transaction..deleteNode(widget.node);
|
||||
widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
FlowyTint get tint {
|
||||
final name = widget.node.attributes[kCalloutAttrColor];
|
||||
return (name is String) ? FlowyTint.fromJson(name) : FlowyTint.tint1;
|
||||
}
|
||||
|
||||
String get emoji {
|
||||
return widget.node.attributes[kCalloutAttrEmoji] ?? "💡";
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart' as highlight;
|
||||
import 'package:highlight/languages/all.dart';
|
||||
|
||||
const String kCodeBlockType = 'text/$kCodeBlockSubType';
|
||||
const String kCodeBlockSubType = 'code_block';
|
||||
const String kCodeBlockAttrTheme = 'theme';
|
||||
const String kCodeBlockAttrLanguage = 'language';
|
||||
|
||||
class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder<TextNode>
|
||||
with ActionProvider<TextNode> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<TextNode> context) {
|
||||
return _CodeBlockNodeWidge(
|
||||
key: context.node.key,
|
||||
textNode: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node is TextNode &&
|
||||
node.attributes[kCodeBlockAttrTheme] is String;
|
||||
};
|
||||
|
||||
@override
|
||||
List<ActionMenuItem> actions(NodeWidgetContext<TextNode> context) {
|
||||
return [
|
||||
ActionMenuItem.svg(
|
||||
name: 'delete',
|
||||
onPressed: () {
|
||||
final transaction = context.editorState.transaction
|
||||
..deleteNode(context.node);
|
||||
context.editorState.apply(transaction);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _CodeBlockNodeWidge extends StatefulWidget {
|
||||
const _CodeBlockNodeWidge({
|
||||
Key? key,
|
||||
required this.textNode,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextNode textNode;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState();
|
||||
}
|
||||
|
||||
class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
|
||||
with SelectableMixin, DefaultSelectable {
|
||||
final _richTextKey = GlobalKey(debugLabel: kCodeBlockType);
|
||||
final _padding = const EdgeInsets.only(left: 20, top: 30, bottom: 30);
|
||||
String? get _language =>
|
||||
widget.textNode.attributes[kCodeBlockAttrLanguage] as String?;
|
||||
String? _detectLanguage;
|
||||
|
||||
@override
|
||||
SelectableMixin<StatefulWidget> get forward =>
|
||||
_richTextKey.currentState as SelectableMixin;
|
||||
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>>? get iconKey => null;
|
||||
|
||||
@override
|
||||
Offset get baseOffset => super.baseOffset + _padding.topLeft;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildCodeBlock(context),
|
||||
_buildSwitchCodeButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeBlock(BuildContext context) {
|
||||
final result = highlight.highlight.parse(
|
||||
widget.textNode.toPlainText(),
|
||||
language: _language,
|
||||
autoDetection: _language == null,
|
||||
);
|
||||
_detectLanguage = _language ?? result.language;
|
||||
final code = result.nodes;
|
||||
final codeTextSpan = _convert(code!);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
),
|
||||
padding: _padding,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: FlowyRichText(
|
||||
key: _richTextKey,
|
||||
textNode: widget.textNode,
|
||||
editorState: widget.editorState,
|
||||
lineHeight: 1.0,
|
||||
cursorHeight: 15.0,
|
||||
textSpanDecorator: (textSpan) => TextSpan(
|
||||
style: widget.editorState.editorStyle.textStyle,
|
||||
children: codeTextSpan,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchCodeButton(BuildContext context) {
|
||||
return Positioned(
|
||||
top: -5,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: DropdownButton<String>(
|
||||
value: _detectLanguage,
|
||||
iconSize: 14.0,
|
||||
onChanged: (value) {
|
||||
final transaction = widget.editorState.transaction
|
||||
..updateNode(widget.textNode, {
|
||||
kCodeBlockAttrLanguage: value,
|
||||
});
|
||||
widget.editorState.apply(transaction);
|
||||
},
|
||||
items:
|
||||
allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: FlowyText.medium(
|
||||
value,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
);
|
||||
}).toList(growable: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Copy from flutter.highlight package.
|
||||
// https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
|
||||
List<TextSpan> _convert(List<highlight.Node> nodes) {
|
||||
List<TextSpan> spans = [];
|
||||
var currentSpans = spans;
|
||||
List<List<TextSpan>> stack = [];
|
||||
|
||||
void traverse(highlight.Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(
|
||||
text: node.value,
|
||||
style: _builtInCodeBlockTheme[node.className!],),);
|
||||
} else if (node.children != null) {
|
||||
List<TextSpan> tmp = [];
|
||||
currentSpans.add(TextSpan(
|
||||
children: tmp, style: _builtInCodeBlockTheme[node.className!],),);
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (var n in node.children!) {
|
||||
traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var node in nodes) {
|
||||
traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
const _builtInCodeBlockTheme = {
|
||||
'root':
|
||||
TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
|
||||
'comment': TextStyle(color: Color(0xff007400)),
|
||||
'quote': TextStyle(color: Color(0xff007400)),
|
||||
'tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'attribute': TextStyle(color: Color(0xffaa0d91)),
|
||||
'keyword': TextStyle(color: Color(0xffaa0d91)),
|
||||
'selector-tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'literal': TextStyle(color: Color(0xffaa0d91)),
|
||||
'name': TextStyle(color: Color(0xffaa0d91)),
|
||||
'variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'template-variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'code': TextStyle(color: Color(0xffc41a16)),
|
||||
'string': TextStyle(color: Color(0xffc41a16)),
|
||||
'meta-string': TextStyle(color: Color(0xffc41a16)),
|
||||
'regexp': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'link': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'title': TextStyle(color: Color(0xff1c00cf)),
|
||||
'symbol': TextStyle(color: Color(0xff1c00cf)),
|
||||
'bullet': TextStyle(color: Color(0xff1c00cf)),
|
||||
'number': TextStyle(color: Color(0xff1c00cf)),
|
||||
'section': TextStyle(color: Color(0xff643820)),
|
||||
'meta': TextStyle(color: Color(0xff643820)),
|
||||
'type': TextStyle(color: Color(0xff5c2699)),
|
||||
'built_in': TextStyle(color: Color(0xff5c2699)),
|
||||
'builtin-name': TextStyle(color: Color(0xff5c2699)),
|
||||
'params': TextStyle(color: Color(0xff5c2699)),
|
||||
'attr': TextStyle(color: Color(0xff836C28)),
|
||||
'subst': TextStyle(color: Color(0xff000000)),
|
||||
'formula': TextStyle(
|
||||
backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic,),
|
||||
'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
|
||||
'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
|
||||
'selector-id': TextStyle(color: Color(0xff9b703f)),
|
||||
'selector-class': TextStyle(color: Color(0xff9b703f)),
|
||||
'doctag': TextStyle(fontWeight: FontWeight.bold),
|
||||
'strong': TextStyle(fontWeight: FontWeight.bold),
|
||||
'emphasis': TextStyle(fontStyle: FontStyle.italic),
|
||||
};
|
@ -1,125 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
ShortcutEvent enterInCodeBlock = ShortcutEvent(
|
||||
key: 'Press Enter In Code Block',
|
||||
command: 'enter',
|
||||
handler: _enterInCodeBlockHandler,
|
||||
);
|
||||
|
||||
ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent(
|
||||
key: 'White space in code block',
|
||||
command: 'space, slash, shift+underscore',
|
||||
handler: _ignorekHandler,
|
||||
);
|
||||
|
||||
ShortcutEvent pasteInCodeBlock = ShortcutEvent(
|
||||
key: 'Paste in code block',
|
||||
command: 'meta+v',
|
||||
windowsCommand: 'ctrl+v',
|
||||
linuxCommand: 'ctrl+v',
|
||||
handler: _pasteHandler,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final codeBlockNode =
|
||||
nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
|
||||
if (codeBlockNode.length != 1 ||
|
||||
selection == null ||
|
||||
!selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
codeBlockNode.first,
|
||||
selection.end.offset,
|
||||
'\n',
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
ShortcutEventHandler _ignorekHandler = (editorState, event) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final codeBlockNodes =
|
||||
nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
|
||||
if (codeBlockNodes.length == 1) {
|
||||
return KeyEventResult.skipRemainingHandlers;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
ShortcutEventHandler _pasteHandler = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final codeBlockNodes =
|
||||
nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
|
||||
if (selection != null &&
|
||||
selection.isCollapsed &&
|
||||
codeBlockNodes.length == 1) {
|
||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||
final text = value?.text;
|
||||
if (text == null) return;
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertText(
|
||||
codeBlockNodes.first,
|
||||
selection.startIndex,
|
||||
text,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||
name: 'Code Block',
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.abc,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['code block', 'code snippet'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final transaction = editorState.transaction;
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toPlainText().isEmpty && textNode.next is TextNode) {
|
||||
transaction.updateNode(textNodes.first, {
|
||||
BuiltInAttributeKey.subtype: kCodeBlockSubType,
|
||||
kCodeBlockAttrTheme: 'vs',
|
||||
kCodeBlockAttrLanguage: null,
|
||||
});
|
||||
transaction.afterSelection = selection;
|
||||
editorState.apply(transaction);
|
||||
} else {
|
||||
transaction.insertNode(
|
||||
selection.end.path,
|
||||
TextNode(
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: kCodeBlockSubType,
|
||||
kCodeBlockAttrTheme: 'vs',
|
||||
kCodeBlockAttrLanguage: null,
|
||||
},
|
||||
delta: Delta()..insert('\n'),
|
||||
),
|
||||
);
|
||||
transaction.afterSelection = selection;
|
||||
}
|
||||
editorState.apply(transaction);
|
||||
},
|
||||
);
|
@ -1,84 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String kDividerType = 'divider';
|
||||
|
||||
class DividerWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _DividerWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
class _DividerWidget extends StatefulWidget {
|
||||
const _DividerWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_DividerWidget> createState() => _DividerWidgetState();
|
||||
}
|
||||
|
||||
class _DividerWidgetState extends State<_DividerWidget> 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);
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/divider/divider_node_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// insert divider into a document by typing three minuses.
|
||||
// ---
|
||||
ShortcutEvent insertDividerEvent = ShortcutEvent(
|
||||
key: 'Divider',
|
||||
command: 'Minus',
|
||||
handler: _insertDividerHandler,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _insertDividerHandler = (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() != '--') {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(textNode, 0, 2) // remove the existing minuses.
|
||||
..insertNode(textNode.path, Node(type: kDividerType)) // insert the divder
|
||||
..afterSelection = Selection.single(
|
||||
// update selection to the next text node.
|
||||
path: textNode.path.next,
|
||||
startOffset: 0,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
SelectionMenuItem dividerMenuItem = SelectionMenuItem(
|
||||
name: 'Divider',
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.horizontal_rule,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['horizontal rule', 'divider'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1 || selection == null) {
|
||||
return;
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
// insert the divider at current path if the text node is empty.
|
||||
if (textNode.toPlainText().isEmpty) {
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(textNode.path, Node(type: kDividerType))
|
||||
..afterSelection = Selection.single(
|
||||
path: textNode.path.next,
|
||||
startOffset: 0,
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
} else {
|
||||
// insert the divider at the path next to current path if the text node is not empty.
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(selection.end.path.next, Node(type: kDividerType))
|
||||
..afterSelection = selection;
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
},
|
||||
);
|
@ -1,180 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
name: 'Emoji',
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.emoji_emotions_outlined,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['emoji'],
|
||||
handler: _showEmojiSelectionMenu,
|
||||
);
|
||||
|
||||
OverlayEntry? _emojiSelectionMenu;
|
||||
EditorState? _editorState;
|
||||
void _showEmojiSelectionMenu(
|
||||
EditorState editorState,
|
||||
SelectionMenuService menuService,
|
||||
BuildContext context,
|
||||
) {
|
||||
final alignment = menuService.alignment;
|
||||
final offset = menuService.offset;
|
||||
menuService.dismiss();
|
||||
|
||||
_emojiSelectionMenu?.remove();
|
||||
_emojiSelectionMenu = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
child: EmojiSelectionMenu(
|
||||
editorState: editorState,
|
||||
onSubmitted: (text) {
|
||||
// insert emoji
|
||||
editorState.insertEmoji(text);
|
||||
},
|
||||
onExit: () {
|
||||
_dismissEmojiSelectionMenu();
|
||||
//close emoji panel
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_emojiSelectionMenu!);
|
||||
|
||||
_editorState = editorState;
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissEmojiSelectionMenu);
|
||||
}
|
||||
|
||||
void _dismissEmojiSelectionMenu() {
|
||||
_emojiSelectionMenu?.remove();
|
||||
_emojiSelectionMenu = null;
|
||||
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissEmojiSelectionMenu);
|
||||
_editorState?.service.keyboardService?.enable();
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
class EmojiSelectionMenu extends StatefulWidget {
|
||||
const EmojiSelectionMenu({
|
||||
Key? key,
|
||||
required this.onSubmitted,
|
||||
required this.onExit,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final void Function(Emoji emoji) onSubmitted;
|
||||
final void Function() onExit;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<EmojiSelectionMenu> createState() => _EmojiSelectionMenuState();
|
||||
}
|
||||
|
||||
class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
EditorStyle get style => widget.editorState.editorStyle;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _handleGlobalKeyEvent(KeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape &&
|
||||
event is KeyDownEvent) {
|
||||
//triggers on esc
|
||||
widget.onExit();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: style.selectionMenuBackgroundColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5,
|
||||
spreadRadius: 1,
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: _buildEmojiBox(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiBox(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor:
|
||||
style.selectionMenuBackgroundColor ?? const Color(0xffF2F2F2),
|
||||
iconColor: Colors.grey,
|
||||
iconColorSelected: const Color(0xff333333),
|
||||
indicatorColor: const Color(0xff333333),
|
||||
progressIndicatorColor: const Color(0xff333333),
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on EditorState {
|
||||
void insertEmoji(Emoji emoji) {
|
||||
final selectionService = service.selectionService;
|
||||
final currentSelection = selectionService.currentSelection.value;
|
||||
final nodes = selectionService.currentSelectedNodes;
|
||||
if (currentSelection == null ||
|
||||
!currentSelection.isCollapsed ||
|
||||
nodes.first is! TextNode) {
|
||||
return;
|
||||
}
|
||||
final textNode = nodes.first as TextNode;
|
||||
final tr = transaction;
|
||||
tr.insertText(
|
||||
textNode,
|
||||
currentSelection.endIndex,
|
||||
emoji.emoji,
|
||||
);
|
||||
apply(tr);
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String kGridType = 'grid';
|
||||
|
||||
class GridNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _GridWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node.attributes[kAppID] is String &&
|
||||
node.attributes[kViewID] is String;
|
||||
};
|
||||
}
|
||||
|
||||
class _GridWidget extends StatefulWidget {
|
||||
const _GridWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_GridWidget> createState() => _GridWidgetState();
|
||||
}
|
||||
|
||||
class _GridWidgetState extends State<_GridWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuiltInPageWidget(
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
builder: (viewPB) {
|
||||
return GridPage(
|
||||
key: ValueKey(viewPB.id),
|
||||
view: viewPB,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_math_fork/flutter_math.dart';
|
||||
|
||||
const String kMathEquationType = 'math_equation';
|
||||
const String kMathEquationAttr = 'math_equation';
|
||||
|
||||
SelectionMenuItem mathEquationMenuItem = SelectionMenuItem(
|
||||
name: 'Math Equation',
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.text_fields_rounded,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['tex, latex, katex', 'math equation'],
|
||||
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;
|
||||
final Path mathEquationNodePath;
|
||||
if (textNode.toPlainText().isEmpty) {
|
||||
mathEquationNodePath = selection.end.path;
|
||||
} else {
|
||||
mathEquationNodePath = selection.end.path.next;
|
||||
}
|
||||
// insert the math equation node
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(
|
||||
mathEquationNodePath,
|
||||
Node(type: kMathEquationType, attributes: {kMathEquationAttr: ''}),
|
||||
)
|
||||
..afterSelection = selection;
|
||||
editorState.apply(transaction);
|
||||
|
||||
// tricy to show the editing dialog.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final mathEquationState = editorState.document
|
||||
.nodeAtPath(mathEquationNodePath)
|
||||
?.key
|
||||
.currentState;
|
||||
if (mathEquationState != null &&
|
||||
mathEquationState is _MathEquationNodeWidgetState) {
|
||||
mathEquationState.showEditingDialog();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
class MathEquationNodeWidgetBuidler extends NodeWidgetBuilder<Node>
|
||||
with ActionProvider<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _MathEquationNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator =>
|
||||
(node) => node.attributes[kMathEquationAttr] is String;
|
||||
|
||||
@override
|
||||
List<ActionMenuItem> actions(NodeWidgetContext<Node> context) {
|
||||
return [
|
||||
ActionMenuItem.svg(
|
||||
name: "delete",
|
||||
onPressed: () {
|
||||
final transaction = context.editorState.transaction
|
||||
..deleteNode(context.node);
|
||||
context.editorState.apply(transaction);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _MathEquationNodeWidget extends StatefulWidget {
|
||||
const _MathEquationNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_MathEquationNodeWidget> createState() =>
|
||||
_MathEquationNodeWidgetState();
|
||||
}
|
||||
|
||||
class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
|
||||
String get _mathEquation =>
|
||||
widget.node.attributes[kMathEquationAttr] as String;
|
||||
bool _isHover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onHover: (value) {
|
||||
setState(() {
|
||||
_isHover = value;
|
||||
});
|
||||
},
|
||||
onTap: () {
|
||||
showEditingDialog();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildMathEquation(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMathEquation(BuildContext context) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(minHeight: 50),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: _isHover || _mathEquation.isEmpty
|
||||
? Theme.of(context).colorScheme.tertiaryContainer
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: Center(
|
||||
child: _mathEquation.isEmpty
|
||||
? FlowyText.medium(
|
||||
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
|
||||
fontSize: 16,
|
||||
)
|
||||
: Math.tex(
|
||||
_mathEquation,
|
||||
textStyle: const TextStyle(fontSize: 20),
|
||||
mathStyle: MathStyle.display,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showEditingDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller = TextEditingController(text: _mathEquation);
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
title: Text(
|
||||
LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
|
||||
),
|
||||
content: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (key) {
|
||||
if (key is! RawKeyDownEvent) return;
|
||||
if (key.logicalKey == LogicalKeyboardKey.enter &&
|
||||
!key.isShiftPressed) {
|
||||
_updateMathEquation(controller.text, context);
|
||||
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_dismiss(context);
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
maxLines: null,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'E = MC^2',
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () => _dismiss(context),
|
||||
),
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_Done.tr(),
|
||||
onPressed: () => _updateMathEquation(controller.text, context),
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.only(bottom: 20),
|
||||
actionsAlignment: MainAxisAlignment.spaceAround,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateMathEquation(String mathEquation, BuildContext context) {
|
||||
if (mathEquation == _mathEquation) {
|
||||
_dismiss(context);
|
||||
return;
|
||||
}
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(
|
||||
widget.node,
|
||||
{
|
||||
kMathEquationAttr: mathEquation,
|
||||
},
|
||||
);
|
||||
widget.editorState.apply(transaction);
|
||||
_dismiss(context);
|
||||
}
|
||||
|
||||
void _dismiss(BuildContext context) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import '../util/editor_extension.dart';
|
||||
|
||||
const String kAutoCompletionInputType = 'auto_completion_input';
|
||||
const String kAutoCompletionInputString = 'auto_completion_input_string';
|
||||
const String kAutoCompletionInputStartSelection =
|
||||
'auto_completion_input_start_selection';
|
||||
|
||||
class AutoCompletionInputBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node.attributes[kAutoCompletionInputString] is String;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _AutoCompletionInput(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoCompletionInput extends StatefulWidget {
|
||||
final Node node;
|
||||
|
||||
final EditorState editorState;
|
||||
const _AutoCompletionInput({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AutoCompletionInput> createState() => _AutoCompletionInputState();
|
||||
}
|
||||
|
||||
class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
String get text => widget.node.attributes[kAutoCompletionInputString];
|
||||
|
||||
final controller = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final textFieldFocusNode = FocusNode();
|
||||
final interceptor = SelectionInterceptor();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
textFieldFocusNode.addListener(_onFocusChanged);
|
||||
textFieldFocusNode.requestFocus();
|
||||
widget.editorState.service.selectionService.register(
|
||||
interceptor
|
||||
..canTap = (details) {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
if (!isTapDownDetailsInRenderBox(details, renderBox)) {
|
||||
if (text.isNotEmpty || controller.text.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => _onDiscard(),
|
||||
onCancel: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (controller.text.isEmpty) {
|
||||
_onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
|
||||
var result = BoxHitTestResult();
|
||||
box.hitTest(result, position: box.globalToLocal(details.globalPosition));
|
||||
return result.path.any((entry) => entry.target == box);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
textFieldFocusNode.removeListener(_onFocusChanged);
|
||||
widget.editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onCancelWhenSelectionChanged);
|
||||
widget.editorState.service.selectionService.unRegister(interceptor);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 5,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: _buildAutoGeneratorPanel(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAutoGeneratorPanel(BuildContext context) {
|
||||
if (text.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeaderWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildInputWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildInputFooterWidget(context),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeaderWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildFooterWidget(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeaderWidget(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputWidget(BuildContext context) {
|
||||
return FlowyTextField(
|
||||
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
focusNode: textFieldFocusNode,
|
||||
autoFocus: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputFooterWidget(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_generate.tr(),
|
||||
onPressed: () async => await _onGenerate(),
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () async => await _onExit(),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooterWidget(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_keep.tr(),
|
||||
onPressed: () => _onExit(),
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_discard.tr(),
|
||||
onPressed: () => _onDiscard(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onExit() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNode(widget.node);
|
||||
await widget.editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
recordRedo: false,
|
||||
recordUndo: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onGenerate() async {
|
||||
final loading = Loading(context);
|
||||
loading.start();
|
||||
await _updateEditingText();
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
|
||||
result.fold((userProfile) async {
|
||||
BarrierDialog? barrierDialog;
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: userProfile.openaiKey,
|
||||
);
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
prompt: controller.text,
|
||||
onStart: () async {
|
||||
loading.stop();
|
||||
barrierDialog = BarrierDialog(context);
|
||||
barrierDialog?.show();
|
||||
await _makeSurePreviousNodeIsEmptyTextNode();
|
||||
},
|
||||
onProcess: (response) async {
|
||||
if (response.choices.isNotEmpty) {
|
||||
final text = response.choices.first.text;
|
||||
await widget.editorState.autoInsertText(
|
||||
text,
|
||||
inputType: TextRobotInputType.word,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
}
|
||||
},
|
||||
onEnd: () async {
|
||||
await barrierDialog?.dismiss();
|
||||
widget.editorState.service.selectionService.currentSelection
|
||||
.addListener(_onCancelWhenSelectionChanged);
|
||||
},
|
||||
onError: (error) async {
|
||||
loading.stop();
|
||||
await _showError(error.message);
|
||||
},
|
||||
);
|
||||
}, (error) async {
|
||||
loading.stop();
|
||||
await _showError(
|
||||
LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onDiscard() async {
|
||||
final selection =
|
||||
widget.node.attributes[kAutoCompletionInputStartSelection];
|
||||
if (selection != null) {
|
||||
final start = Selection.fromJson(json.decode(selection)).start.path;
|
||||
final end = widget.node.previous?.path;
|
||||
if (end != null) {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNodesAtPath(
|
||||
start,
|
||||
end.last - start.last + 1,
|
||||
);
|
||||
await widget.editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
_onExit();
|
||||
}
|
||||
|
||||
Future<void> _updateEditingText() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.updateNode(
|
||||
widget.node,
|
||||
{
|
||||
kAutoCompletionInputString: controller.text,
|
||||
},
|
||||
);
|
||||
await widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _makeSurePreviousNodeIsEmptyTextNode() async {
|
||||
// make sure the previous node is a empty text node without any styles.
|
||||
final transaction = widget.editorState.transaction;
|
||||
final Selection selection;
|
||||
if (widget.node.previous is! TextNode ||
|
||||
(widget.node.previous as TextNode).toPlainText().isNotEmpty ||
|
||||
(widget.node.previous as TextNode).subtype != null) {
|
||||
transaction.insertNode(
|
||||
widget.node.path,
|
||||
TextNode.empty(),
|
||||
);
|
||||
selection = Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
);
|
||||
transaction.afterSelection = selection;
|
||||
} else {
|
||||
selection = Selection.single(
|
||||
path: widget.node.path.previous,
|
||||
startOffset: 0,
|
||||
);
|
||||
transaction.afterSelection = selection;
|
||||
}
|
||||
transaction.updateNode(widget.node, {
|
||||
kAutoCompletionInputStartSelection: jsonEncode(selection.toJson()),
|
||||
});
|
||||
await widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _showError(String message) async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
action: SnackBarAction(
|
||||
label: LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
content: FlowyText(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (textFieldFocusNode.hasFocus) {
|
||||
widget.editorState.service.keyboardService?.disable(
|
||||
disposition: UnfocusDisposition.previouslyFocusedChild,
|
||||
);
|
||||
} else {
|
||||
widget.editorState.service.keyboardService?.enable();
|
||||
}
|
||||
}
|
||||
|
||||
void _onCancelWhenSelectionChanged() {}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
|
||||
iconData: Icons.generating_tokens,
|
||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
||||
nodeBuilder: (editorState) {
|
||||
final node = Node(
|
||||
type: kAutoCompletionInputType,
|
||||
attributes: {
|
||||
kAutoCompletionInputString: '',
|
||||
},
|
||||
);
|
||||
return node;
|
||||
},
|
||||
replace: (_, textNode) => textNode.toPlainText().isEmpty,
|
||||
updateSelection: null,
|
||||
);
|
@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Loading {
|
||||
Loading(
|
||||
this.context,
|
||||
);
|
||||
|
||||
late BuildContext loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> start() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SimpleDialog(
|
||||
elevation: 0.0,
|
||||
backgroundColor:
|
||||
Colors.transparent, // can change this to your preferred color
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
return Navigator.of(loadingContext).pop();
|
||||
}
|
||||
}
|
||||
|
||||
class BarrierDialog {
|
||||
BarrierDialog(
|
||||
this.context,
|
||||
);
|
||||
|
||||
late BuildContext loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> show() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dismiss() async {
|
||||
return Navigator.of(loadingContext).pop();
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
|
||||
@ -52,13 +52,13 @@ void _resolveCommonService(GetIt getIt) async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold(
|
||||
(l) {
|
||||
return HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: l.openaiKey,
|
||||
);
|
||||
throw Exception('Failed to get user profile: ${l.msg}');
|
||||
},
|
||||
(r) {
|
||||
throw Exception('Failed to get user profile: ${r.msg}');
|
||||
return HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: r.openaiKey,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -14,8 +14,10 @@ class UserBackendService {
|
||||
|
||||
final Int64 userId;
|
||||
|
||||
static Future<Either<UserProfilePB, FlowyError>> getCurrentUserProfile() {
|
||||
return UserEventGetUserProfile().send();
|
||||
static Future<Either<FlowyError, UserProfilePB>>
|
||||
getCurrentUserProfile() async {
|
||||
final result = await UserEventGetUserProfile().send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateUserProfile({
|
||||
|
5
frontend/appflowy_flutter/lib/util/base64_string.dart
Normal file
5
frontend/appflowy_flutter/lib/util/base64_string.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
extension Base64Encode on String {
|
||||
String get base64 => base64Encode(utf8.encode(this));
|
||||
}
|
8
frontend/appflowy_flutter/lib/util/json_print.dart
Normal file
8
frontend/appflowy_flutter/lib/util/json_print.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
|
||||
const JsonEncoder _encoder = JsonEncoder.withIndent(' ');
|
||||
void prettyPrintJson(Object? object) {
|
||||
Log.debug(_encoder.convert(object));
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:appflowy/core/document_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
|
||||
import 'package:appflowy_backend/rust_stream.dart';
|
||||
|
||||
class DocumentListener {
|
||||
@ -17,10 +17,10 @@ class DocumentListener {
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
DocumentNotificationParser? _parser;
|
||||
|
||||
Function()? didReceiveUpdate;
|
||||
Function(DocEventPB docEvent)? didReceiveUpdate;
|
||||
|
||||
void start({
|
||||
void Function()? didReceiveUpdate,
|
||||
Function(DocEventPB docEvent)? didReceiveUpdate,
|
||||
}) {
|
||||
this.didReceiveUpdate = didReceiveUpdate;
|
||||
|
||||
@ -39,7 +39,9 @@ class DocumentListener {
|
||||
) {
|
||||
switch (ty) {
|
||||
case DocumentNotification.DidReceiveUpdate:
|
||||
didReceiveUpdate?.call();
|
||||
result
|
||||
.swap()
|
||||
.map((r) => didReceiveUpdate?.call(DocEventPB.fromBuffer(r)));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@ -11,6 +11,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
||||
final BoxConstraints constraints;
|
||||
final PopoverDirection direction;
|
||||
final Widget Function(PopoverController) buildChild;
|
||||
final VoidCallback? onPopupBuilder;
|
||||
final VoidCallback? onClosed;
|
||||
final bool asBarrier;
|
||||
final Offset offset;
|
||||
@ -21,6 +22,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
|
||||
required this.onSelected,
|
||||
this.mutex,
|
||||
this.onClosed,
|
||||
this.onPopupBuilder,
|
||||
this.direction = PopoverDirection.rightWithTopAligned,
|
||||
this.asBarrier = false,
|
||||
this.offset = Offset.zero,
|
||||
@ -60,6 +62,7 @@ class _PopoverActionListState<T extends PopoverAction>
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
onClose: widget.onClosed,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
widget.onPopupBuilder?.call();
|
||||
final List<Widget> children = widget.actions.map((action) {
|
||||
if (action is ActionCell) {
|
||||
return ActionCellWidget<T>(
|
||||
@ -69,6 +72,13 @@ class _PopoverActionListState<T extends PopoverAction>
|
||||
widget.onSelected(action, popoverController);
|
||||
},
|
||||
);
|
||||
} else if (action is PopoverActionCell) {
|
||||
return PopoverActionCellWidget<T>(
|
||||
mutex: widget.mutex,
|
||||
// popoverController: popoverController,
|
||||
action: action,
|
||||
itemHeight: ActionListSizes.itemHeight,
|
||||
);
|
||||
} else {
|
||||
final custom = action as CustomActionCell;
|
||||
return custom.buildWithContext(context);
|
||||
@ -94,6 +104,15 @@ abstract class ActionCell extends PopoverAction {
|
||||
String get name;
|
||||
}
|
||||
|
||||
abstract class PopoverActionCell extends PopoverAction {
|
||||
Widget? leftIcon(Color iconColor) => null;
|
||||
Widget? rightIcon(Color iconColor) => null;
|
||||
String get name;
|
||||
|
||||
Widget Function(BuildContext context, PopoverController controller)
|
||||
get builder;
|
||||
}
|
||||
|
||||
abstract class CustomActionCell extends PopoverAction {
|
||||
Widget buildWithContext(BuildContext context);
|
||||
}
|
||||
@ -127,27 +146,95 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
|
||||
final rightIcon =
|
||||
actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
return _HoverButton(
|
||||
itemHeight: itemHeight,
|
||||
leftIcon: leftIcon,
|
||||
rightIcon: rightIcon,
|
||||
name: actionCell.name,
|
||||
onTap: () => onSelected(action),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverActionCellWidget<T extends PopoverAction> extends StatelessWidget {
|
||||
PopoverActionCellWidget({
|
||||
Key? key,
|
||||
this.mutex,
|
||||
// required this.popoverController,
|
||||
required this.action,
|
||||
required this.itemHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
final T action;
|
||||
final double itemHeight;
|
||||
|
||||
final PopoverMutex? mutex;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actionCell = action as PopoverActionCell;
|
||||
final leftIcon =
|
||||
actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);
|
||||
final rightIcon =
|
||||
actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
|
||||
return AppFlowyPopover(
|
||||
// mutex: mutex,
|
||||
controller: popoverController,
|
||||
asBarrier: true,
|
||||
popupBuilder: (context) => actionCell.builder(
|
||||
context,
|
||||
popoverController,
|
||||
),
|
||||
child: _HoverButton(
|
||||
itemHeight: itemHeight,
|
||||
leftIcon: leftIcon,
|
||||
rightIcon: rightIcon,
|
||||
name: actionCell.name,
|
||||
onTap: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HoverButton extends StatelessWidget {
|
||||
const _HoverButton({
|
||||
required this.onTap,
|
||||
required this.itemHeight,
|
||||
required this.leftIcon,
|
||||
required this.name,
|
||||
required this.rightIcon,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final double itemHeight;
|
||||
final Widget? leftIcon;
|
||||
final Widget? rightIcon;
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyHover(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => onSelected(action),
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: itemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leftIcon != null) ...[
|
||||
leftIcon,
|
||||
leftIcon!,
|
||||
HSpace(ActionListSizes.itemHPadding)
|
||||
],
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
actionCell.name,
|
||||
name,
|
||||
overflow: TextOverflow.visible,
|
||||
),
|
||||
),
|
||||
if (rightIcon != null) ...[
|
||||
HSpace(ActionListSizes.itemHPadding),
|
||||
rightIcon,
|
||||
rightIcon!,
|
||||
],
|
||||
],
|
||||
),
|
||||
|
@ -44,10 +44,11 @@ packages:
|
||||
appflowy_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: appflowy_editor
|
||||
sha256: "6f7d2b0b54ca1049cb396229549d228b5bbd7ea6d09f1f7325a20db2d7586a5f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: "4f66f7"
|
||||
resolved-ref: "4f66f77debabbc35cf4a56c816f9432a831a40e2"
|
||||
url: "https://github.com/LucasXu0/appflowy-editor.git"
|
||||
source: git
|
||||
version: "0.1.12"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
@ -816,6 +817,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
nanoid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: nanoid
|
||||
sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -42,7 +42,12 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: a183c57
|
||||
appflowy_editor: ^0.1.12
|
||||
# appflowy_editor: ^0.1.9
|
||||
appflowy_editor:
|
||||
# path: /Users/lucas.xu/Desktop/appflowy-editor
|
||||
git:
|
||||
url: https://github.com/LucasXu0/appflowy-editor.git
|
||||
ref: 4f66f7
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
@ -101,6 +106,7 @@ dependencies:
|
||||
mocktail: ^0.3.0
|
||||
archive: ^3.3.0
|
||||
flutter_svg: ^2.0.5
|
||||
nanoid: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.1
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -12,7 +12,7 @@ void main() {
|
||||
const text = '''
|
||||
{
|
||||
"document":{
|
||||
"type":"editor",
|
||||
"type":"document",
|
||||
"children":[
|
||||
{
|
||||
"type":"math_equation",
|
||||
|
595
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
595
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -96,6 +96,21 @@ version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"collab",
|
||||
"collab-database",
|
||||
"collab-document",
|
||||
"collab-folder",
|
||||
"collab-persistence",
|
||||
"collab-plugins",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "appflowy_tauri"
|
||||
version = "0.0.0"
|
||||
@ -193,6 +208,324 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-http",
|
||||
"aws-sdk-sso",
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"hex",
|
||||
"http",
|
||||
"hyper",
|
||||
"ring",
|
||||
"time 0.3.20",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
"fastrand",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-endpoint"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"http",
|
||||
"regex",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-http"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-dynamodb"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67fb64867fe098cffee7e34352b01bbfa2beb3aa1b2ff0e0a7bf9ff293557852"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-endpoint",
|
||||
"aws-http",
|
||||
"aws-sig-auth",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http",
|
||||
"regex",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-endpoint",
|
||||
"aws-http",
|
||||
"aws-sig-auth",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"http",
|
||||
"regex",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-endpoint",
|
||||
"aws-http",
|
||||
"aws-sig-auth",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-query",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-xml",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"http",
|
||||
"regex",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sig-auth"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-http",
|
||||
"aws-types",
|
||||
"http",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"form_urlencoded",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"sha2",
|
||||
"time 0.3.20",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-client"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-tower",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"lazy_static",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-tower"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-query"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"itoa 1.0.6",
|
||||
"num-integer",
|
||||
"ryu",
|
||||
"time 0.3.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c"
|
||||
dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "0.55.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-client",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"http",
|
||||
"rustc_version",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.67"
|
||||
@ -220,6 +553,16 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||
|
||||
[[package]]
|
||||
name = "base64-simd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@ -419,6 +762,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.11+1.0.8"
|
||||
@ -533,6 +886,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.45",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
@ -662,7 +1016,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -679,7 +1033,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -697,9 +1051,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"collab",
|
||||
"collab-derive",
|
||||
@ -720,7 +1075,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -732,7 +1087,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -749,7 +1104,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -767,7 +1122,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -787,12 +1142,24 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-dynamodb",
|
||||
"collab",
|
||||
"collab-client-ws",
|
||||
"collab-persistence",
|
||||
"collab-sync",
|
||||
"futures-util",
|
||||
"parking_lot 0.12.1",
|
||||
"rand 0.8.5",
|
||||
"rusoto_credential",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-retry",
|
||||
"tracing",
|
||||
"y-sync",
|
||||
"yrs",
|
||||
@ -801,7 +1168,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
@ -1201,6 +1568,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1497,8 +1865,8 @@ dependencies = [
|
||||
name = "flowy-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"appflowy-integrate",
|
||||
"bytes",
|
||||
"collab-persistence",
|
||||
"database-model",
|
||||
"flowy-client-ws",
|
||||
"flowy-database2",
|
||||
@ -1531,13 +1899,14 @@ name = "flowy-database2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"appflowy-integrate",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz 0.8.2",
|
||||
"collab",
|
||||
"collab-database",
|
||||
"collab-plugins",
|
||||
"dashmap",
|
||||
"database-model",
|
||||
"fancy-regex 0.10.0",
|
||||
@ -1624,10 +1993,10 @@ dependencies = [
|
||||
name = "flowy-document2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"appflowy-integrate",
|
||||
"bytes",
|
||||
"collab",
|
||||
"collab-document",
|
||||
"collab-plugins",
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
"flowy-error",
|
||||
@ -1651,6 +2020,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"collab-database",
|
||||
"collab-document",
|
||||
"flowy-client-sync",
|
||||
"flowy-client-ws",
|
||||
"flowy-codegen",
|
||||
@ -1673,11 +2043,11 @@ dependencies = [
|
||||
name = "flowy-folder2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"appflowy-integrate",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"collab-plugins",
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
"flowy-document",
|
||||
@ -1856,8 +2226,8 @@ dependencies = [
|
||||
name = "flowy-user"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"appflowy-integrate",
|
||||
"bytes",
|
||||
"collab-persistence",
|
||||
"diesel",
|
||||
"diesel_derives",
|
||||
"flowy-codegen",
|
||||
@ -2412,6 +2782,21 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.25.2"
|
||||
@ -2509,6 +2894,21 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
@ -3446,6 +3846,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@ -4252,6 +4658,21 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.41"
|
||||
@ -4287,6 +4708,24 @@ dependencies = [
|
||||
"librocksdb-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusoto_credential"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"dirs-next",
|
||||
"futures",
|
||||
"hyper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.29.1"
|
||||
@ -4350,6 +4789,39 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"sct",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.12"
|
||||
@ -4423,6 +4895,16 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@ -4779,6 +5261,12 @@ dependencies = [
|
||||
"system-deps 5.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@ -4850,6 +5338,12 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@ -5348,6 +5842,17 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.14"
|
||||
@ -5442,6 +5947,28 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
@ -5728,6 +6255,12 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
@ -5740,6 +6273,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||
|
||||
[[package]]
|
||||
name = "user-model"
|
||||
version = "0.1.0"
|
||||
@ -5820,6 +6359,12 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.3"
|
||||
@ -5981,6 +6526,16 @@ dependencies = [
|
||||
"system-deps 6.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.19.1"
|
||||
@ -6399,6 +6954,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
|
||||
|
||||
[[package]]
|
||||
name = "y-sync"
|
||||
version = "0.3.1"
|
||||
@ -6433,6 +6994,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.8+zstd.1.5.5"
|
||||
|
1
frontend/rust-lib/Cargo.lock
generated
1
frontend/rust-lib/Cargo.lock
generated
@ -1920,6 +1920,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"collab-database",
|
||||
"collab-document",
|
||||
"flowy-client-sync",
|
||||
"flowy-client-ws",
|
||||
"flowy-codegen",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user