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:
Lucas.Xu 2023-05-16 14:58:24 +08:00 committed by GitHub
parent 99c48f7100
commit 2202326278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 5000 additions and 3287 deletions

View file

@ -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(),
],
);
}
Widget _renderBanner(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,
if (state.isDeleted) _buildBanner(context),
_buildCoverAndIcon(context),
Expanded(
child: appflowyEditorPage,
),
),
],
);
}
@override
void dispose() {
_clearTemporaryNodes();
super.dispose();
Widget _buildBanner(BuildContext context) {
return DocumentBanner(
onRestore: () => documentBloc.add(const DocumentEvent.restorePage()),
onDelete: () => documentBloc.add(const DocumentEvent.deletePermanently()),
);
}
Future<void> _clearTemporaryNodes() async {
final document = editorState.document;
if (document.root.children.isEmpty) {
Widget _buildCoverAndIcon(BuildContext context) {
if (editorState == null) {
return const Placeholder();
}
final page = editorState!.document.root;
return CoverImageNodeWidget(
node: page,
editorState: editorState!,
);
}
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),
);
}
return const dartz.Tuple2(false, null);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText(message),
),
);
}
}