diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 972a19dcfd..c91b688162 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 ndkVersion "24.0.8215888" compileOptions { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index ef84466225..101a546294 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -19,7 +19,6 @@ class MobileBottomNavigationBar extends StatelessWidget { final style = Theme.of(context); return Scaffold( - backgroundColor: Colors.red, body: navigationShell, bottomNavigationBar: BottomNavigationBar( showSelectedLabels: false, diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 665b40626b..5ad72b35fe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -59,7 +59,7 @@ class DocumentBloc extends Bloc { await _viewListener.stop(); await _subscription?.cancel(); await _documentService.closeDocument(view: view); - state.editorState?.selection = null; + state.editorState?.service.keyboardService?.closeKeyboard(); state.editorState?.dispose(); return super.close(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index ae2a71965d..1ae12b4f64 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -22,6 +22,7 @@ import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; final List commandShortcutEvents = [ @@ -215,6 +216,8 @@ class _AppFlowyEditorPageState extends State { @override void dispose() { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + if (widget.scrollController == null) { effectiveScrollController.dispose(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart index b1e26ad3a9..2e5140f9b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart @@ -228,7 +228,7 @@ bool _unSelectable( return false; } -extension on EditorState { +extension EditorStateAddBlock on EditorState { Future insertBlockOrReplaceCurrentBlock( Selection selection, Node insertedNode, @@ -257,7 +257,8 @@ extension on EditorState { ..deleteNode(node) ..afterSelection = Selection.collapsed( Position(path: path, offset: 0), - ); + ) + ..selectionExtraInfo = null; } await apply(transaction); service.keyboardService?.enableKeyBoard(selection); @@ -319,10 +320,10 @@ extension on EditorState { paragraphNode(), ); } + transaction.selectionExtraInfo = {}; transaction.afterSelection = Selection.collapsed( Position(path: insertedPath.next), ); await apply(transaction); - service.keyboardService?.enableKeyBoard(selection); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart index 3db70055d4..3526bde45c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart @@ -1,11 +1,328 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; final addBlockToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { + itemBuilder: (context, editorState, service, __, onAction) { return AppFlowyMobileToolbarIconItem( icon: FlowySvgs.m_toolbar_add_s, - onTap: () {}, + onTap: () { + final selection = editorState.selection; + service.closeKeyboard(); + + // delay to wait the keyboard closed. + Future.delayed(const Duration(milliseconds: 100), () async { + editorState.updateSelectionWithReason( + selection, + extraInfo: { + selectionExtraInfoDisableMobileToolbarKey: true, + selectionExtraInfoDisableFloatingToolbar: true, + selectionExtraInfoDoNotAttachTextService: true, + }, + ); + keepEditorFocusNotifier.increase(); + final didAddBlock = await showAddBlockMenu( + AppGlobals.rootNavKey.currentContext!, + editorState: editorState, + selection: selection!, + ); + if (didAddBlock != true) { + editorState.updateSelectionWithReason( + selection, + ); + } + }); + }, ); }, ); + +Future showAddBlockMenu( + BuildContext context, { + required EditorState editorState, + required Selection selection, +}) async { + final theme = ToolbarColorExtension.of(context); + return showMobileBottomSheet( + context, + showHeader: true, + showCloseButton: true, + showDivider: false, + showDragHandle: true, + barrierColor: Colors.transparent, + backgroundColor: theme.toolbarMenuBackgroundColor, + elevation: 20, + title: LocaleKeys.button_add.tr(), + padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), + builder: (context) { + return Padding( + padding: const EdgeInsets.only(top: 12.0, bottom: 16), + child: _AddBlockMenu( + selection: selection, + editorState: editorState, + ), + ); + }, + ); +} + +class _AddBlockMenu extends StatelessWidget { + _AddBlockMenu({ + required this.selection, + required this.editorState, + }); + + final Selection selection; + final EditorState editorState; + + late final _menuItemData = [ + // paragraph + _AddBlockMenuItemData( + blockType: ParagraphBlockKeys.type, + backgroundColor: const Color(0xFFBAC9FF), + text: LocaleKeys.editor_text.tr(), + icon: FlowySvgs.m_add_block_paragraph_s, + onTap: () => _insertBlock(paragraphNode()), + ), + + // heading 1 - 3 + _AddBlockMenuItemData( + blockType: HeadingBlockKeys.type, + backgroundColor: const Color(0xFFBAC9FF), + text: LocaleKeys.editor_heading1.tr(), + icon: FlowySvgs.m_add_block_h1_s, + onTap: () => _insertBlock(headingNode(level: 1)), + ), + _AddBlockMenuItemData( + blockType: HeadingBlockKeys.type, + backgroundColor: const Color(0xFFBAC9FF), + text: LocaleKeys.editor_heading2.tr(), + icon: FlowySvgs.m_add_block_h2_s, + onTap: () => _insertBlock(headingNode(level: 2)), + ), + _AddBlockMenuItemData( + blockType: HeadingBlockKeys.type, + backgroundColor: const Color(0xFFBAC9FF), + text: LocaleKeys.editor_heading3.tr(), + icon: FlowySvgs.m_add_block_h3_s, + onTap: () => _insertBlock(headingNode(level: 3)), + ), + + // checkbox + _AddBlockMenuItemData( + blockType: TodoListBlockKeys.type, + backgroundColor: const Color(0xFF91EAF5), + text: LocaleKeys.editor_checkbox.tr(), + icon: FlowySvgs.m_add_block_checkbox_s, + onTap: () => _insertBlock(todoListNode(checked: false)), + ), + + // list: bulleted, numbered, toggle + _AddBlockMenuItemData( + blockType: BulletedListBlockKeys.type, + backgroundColor: const Color(0xFFFFB9EF), + text: LocaleKeys.editor_bulletedList.tr(), + icon: FlowySvgs.m_add_block_bulleted_list_s, + onTap: () => _insertBlock(bulletedListNode()), + ), + _AddBlockMenuItemData( + blockType: NumberedListBlockKeys.type, + backgroundColor: const Color(0xFFFFB9EF), + text: LocaleKeys.editor_numberedList.tr(), + icon: FlowySvgs.m_add_block_numbered_list_s, + onTap: () => _insertBlock(numberedListNode()), + ), + _AddBlockMenuItemData( + blockType: ToggleListBlockKeys.type, + backgroundColor: const Color(0xFFFFB9EF), + text: LocaleKeys.document_plugins_toggleList.tr(), + icon: FlowySvgs.m_add_block_toggle_s, + onTap: () => _insertBlock(toggleListBlockNode()), + ), + + // callout, code, math equation, quote + _AddBlockMenuItemData( + blockType: CalloutBlockKeys.type, + backgroundColor: const Color(0xFFCABDFF), + text: LocaleKeys.document_plugins_callout.tr(), + icon: FlowySvgs.m_add_block_callout_s, + onTap: () => _insertBlock(calloutNode()), + ), + _AddBlockMenuItemData( + blockType: CodeBlockKeys.type, + backgroundColor: const Color(0xFFCABDFF), + text: LocaleKeys.document_selectionMenu_codeBlock.tr(), + icon: FlowySvgs.m_add_block_code_s, + onTap: () => _insertBlock(codeBlockNode()), + ), + _AddBlockMenuItemData( + blockType: MathEquationBlockKeys.type, + backgroundColor: const Color(0xFFCABDFF), + text: LocaleKeys.document_plugins_mathEquation_name.tr(), + icon: FlowySvgs.m_add_block_formula_s, + onTap: () { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertMathEquation(selection); + }); + }, + ), + _AddBlockMenuItemData( + blockType: QuoteBlockKeys.type, + backgroundColor: const Color(0xFFFDEDA7), + text: LocaleKeys.editor_quote.tr(), + icon: FlowySvgs.m_add_block_quote_s, + onTap: () => _insertBlock(quoteNode()), + ), + + // divider, + _AddBlockMenuItemData( + blockType: DividerBlockKeys.type, + backgroundColor: const Color(0xFF98F4CD), + text: LocaleKeys.editor_divider.tr(), + icon: FlowySvgs.m_add_block_divider_s, + onTap: () { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertDivider(selection); + }); + }, + ), + ]; + + @override + Widget build(BuildContext context) { + return _GridView( + mainAxisSpacing: 20 * context.scale, + itemWidth: 68.0 * context.scale, + crossAxisCount: 4, + children: _menuItemData.map((e) => _AddBlockMenuItem(data: e)).toList(), + ); + } + + Future _insertBlock(Node node) async { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertBlockAfterCurrentSelection( + selection, + node, + ); + }); + } +} + +class _AddBlockMenuItemData { + const _AddBlockMenuItemData({ + required this.blockType, + required this.backgroundColor, + required this.text, + required this.icon, + required this.onTap, + }); + + final String blockType; + final Color backgroundColor; + final String text; + final FlowySvgData icon; + final VoidCallback onTap; +} + +class _AddBlockMenuItem extends StatelessWidget { + const _AddBlockMenuItem({ + required this.data, + }); + + final _AddBlockMenuItemData data; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: data.onTap, + child: Column( + children: [ + Container( + height: 68.0 * context.scale, + width: 68.0 * context.scale, + decoration: ShapeDecoration( + color: data.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + padding: const EdgeInsets.all(24), + child: FlowySvg( + data.icon, + color: Colors.black, + ), + ), + const VSpace(4), + FlowyText( + data.text, + fontSize: 13.0, + ), + ], + ), + ); + } +} + +class _GridView extends StatelessWidget { + const _GridView({ + required this.children, + required this.crossAxisCount, + required this.mainAxisSpacing, + required this.itemWidth, + }); + + final List children; + final int crossAxisCount; + final double mainAxisSpacing; + final double itemWidth; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < children.length; i += crossAxisCount) + Padding( + padding: EdgeInsets.only(bottom: mainAxisSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + for (var j = 0; j < crossAxisCount; j++) + i + j < children.length ? children[i + j] : HSpace(itemWidth), + ], + ), + ), + ], + ); + } +} + +extension on EditorState { + Future insertBlockAfterCurrentSelection( + Selection selection, + Node node, + ) async { + final path = selection.end.path.next; + final transaction = this.transaction; + transaction.insertNode( + path, + node, + ); + transaction.afterSelection = Selection.collapsed( + Position(path: path, offset: 0), + ); + transaction.selectionExtraInfo = {}; + await apply(transaction); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart index b016b82085..82e42cceb5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_close_keyboard_or_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; @@ -8,6 +9,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -371,6 +373,12 @@ class _MobileToolbarState extends State<_MobileToolbar> // close the keyboard and clear the selection // if the selection is null, the keyboard and the toolbar will be hidden automatically widget.editorState.selection = null; + + // sometimes, the keyboard is not closed after the selection is cleared + if (Platform.isAndroid) { + SystemChannels.textInput + .invokeMethod('TextInput.hide'); + } } }, ), diff --git a/frontend/resources/flowy_icons/16x/m_add_block_bulleted_list.svg b/frontend/resources/flowy_icons/16x/m_add_block_bulleted_list.svg new file mode 100644 index 0000000000..50662aa7e2 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_bulleted_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_callout.svg b/frontend/resources/flowy_icons/16x/m_add_block_callout.svg new file mode 100644 index 0000000000..9f65c59c22 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_callout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_checkbox.svg b/frontend/resources/flowy_icons/16x/m_add_block_checkbox.svg new file mode 100644 index 0000000000..f1c5ea24a4 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_code.svg b/frontend/resources/flowy_icons/16x/m_add_block_code.svg new file mode 100644 index 0000000000..e9825fe194 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_divider.svg b/frontend/resources/flowy_icons/16x/m_add_block_divider.svg new file mode 100644 index 0000000000..6c0d489424 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_formula.svg b/frontend/resources/flowy_icons/16x/m_add_block_formula.svg new file mode 100644 index 0000000000..3a7b09937b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_formula.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_h1.svg b/frontend/resources/flowy_icons/16x/m_add_block_h1.svg new file mode 100644 index 0000000000..7e390a7ff6 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_h2.svg b/frontend/resources/flowy_icons/16x/m_add_block_h2.svg new file mode 100644 index 0000000000..e9d6aca012 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_h3.svg b/frontend/resources/flowy_icons/16x/m_add_block_h3.svg new file mode 100644 index 0000000000..2caeba66d4 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_numbered_list.svg b/frontend/resources/flowy_icons/16x/m_add_block_numbered_list.svg new file mode 100644 index 0000000000..03583c1a0f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_numbered_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_paragraph.svg b/frontend/resources/flowy_icons/16x/m_add_block_paragraph.svg new file mode 100644 index 0000000000..fbe94182cb --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_paragraph.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_quote.svg b/frontend/resources/flowy_icons/16x/m_add_block_quote.svg new file mode 100644 index 0000000000..236d7fd1a8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_add_block_toggle.svg b/frontend/resources/flowy_icons/16x/m_add_block_toggle.svg new file mode 100644 index 0000000000..3d31174900 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_toggle.svg @@ -0,0 +1,4 @@ + + + +