mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 22:57:12 -04: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
125 changed files with 5000 additions and 3287 deletions
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue