From 8116ea1dbabfa70f1b02c9b8a83c784e05f3a27b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 Nov 2023 15:24:32 +0800 Subject: [PATCH] feat: adjust math_equation block and image block on mobile platform (#3890) * feat: add image toolbar entry * feat: add ... buttos on math_equation and image block * fix: review issues * feat: add copy link and save image to gallery * feat: support redo / undo on mobile toolbar --- frontend/appflowy_flutter/ios/Podfile.lock | 12 + .../appflowy_flutter/ios/Runner/Info.plist | 4 +- .../mobile/application/mobile_theme_data.dart | 2 + .../presentation/base/mobile_view_page.dart | 12 +- .../bottom_sheet_action_widget.dart | 1 + .../bottom_sheet_block_action_widget.dart | 78 ++++++ .../bottom_sheet/bottom_sheet_view_page.dart | 50 ++-- .../lib/plugins/document/document_page.dart | 22 +- .../presentation/editor_configuration.dart | 4 +- .../document/presentation/editor_page.dart | 6 +- .../actions/mobile_block_action_buttons.dart | 107 +++++++++ .../copy_and_paste/clipboard_service.dart | 6 + .../image/custom_image_block_component.dart | 222 ++++++++++++++---- .../image/embed_image_url_widget.dart | 1 + .../image/image_placeholder.dart | 155 ++++++++---- .../image/mobile_image_toolbar_item.dart | 17 ++ .../image/unsupport_image_widget.dart | 43 ++++ .../image/upload_image_file_widget.dart | 43 ++-- .../image/upload_image_menu.dart | 15 +- .../math_equation_block_component.dart | 81 +++++-- .../mobile_math_eqaution_toolbar_item.dart | 43 ++++ .../presentation/editor_plugins/plugins.dart | 4 + .../undo_redo/redo_mobile_toolbar_item.dart | 9 + .../undo_redo/undo_mobile_toolbar_item.dart | 9 + .../workspace/presentation/home/toast.dart | 14 +- .../lib/file_picker/file_picker_service.dart | 4 +- .../lib/style_widget/button.dart | 18 +- frontend/appflowy_flutter/pubspec.lock | 123 +++++++++- frontend/appflowy_flutter/pubspec.yaml | 7 +- .../flowy_icons/32x/m_toolbar_imae.svg | 3 + frontend/resources/translations/en.json | 14 +- 31 files changed, 928 insertions(+), 201 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart create mode 100644 frontend/resources/flowy_icons/32x/m_toolbar_imae.svg diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 47e63e5fe9..84858fb29a 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -48,6 +48,10 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - image_gallery_saver (2.0.2): + - Flutter + - image_picker_ios (0.0.1): + - Flutter - integration_test (0.0.1): - Flutter - irondash_engine_context (0.0.1): @@ -86,6 +90,8 @@ DEPENDENCIES: - flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -123,6 +129,10 @@ EXTERNAL SOURCES: :path: Flutter fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" irondash_engine_context: @@ -155,6 +165,8 @@ SPEC CHECKSUMS: flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index ce7ee6a241..91ee44ca33 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -2,8 +2,10 @@ + NSCameraUsageDescription + AppFlowy requires access to the camera. NSPhotoLibraryUsageDescription - This app requires access to the photo library. + AppFlowy requires access to the photo library. CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart index 0b4557f9fe..7d24ebc0a2 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart @@ -34,6 +34,7 @@ ThemeData getMobileThemeData( //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color + surfaceVariant: const Color.fromARGB(255, 216, 216, 216), ) : ColorScheme( brightness: brightness, @@ -223,6 +224,7 @@ ThemeData getMobileThemeData( ), ), colorScheme: mobileColorTheme, + indicatorColor: Colors.blue, extensions: [ AFThemeExtension( warning: theme.yellow, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index bf798a1105..99fef7559e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; +import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -178,12 +179,21 @@ class _MobileViewPageState extends State { context.read().add(FavoriteEvent.toggle(view)); break; case MobileViewBottomSheetBodyAction.undo: + context.dispatchNotification( + const EditorNotification(type: EditorNotificationType.redo), + ); + context.pop(); + break; case MobileViewBottomSheetBodyAction.redo: + context.pop(); + context.dispatchNotification(EditorNotification.redo()); + break; case MobileViewBottomSheetBodyAction.helpCenter: // unimplemented context.pop(); break; case MobileViewBottomSheetBodyAction.rename: + // no need to implement, rename is handled by the onRename callback. throw UnimplementedError(); } }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index b4aa9deee1..2bc843cc0a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -24,6 +24,7 @@ class BottomSheetActionWidget extends StatelessWidget { icon: FlowySvg( svg, size: const Size.square(22.0), + blendMode: BlendMode.dst, color: iconColor, ), label: Text(text), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart new file mode 100644 index 0000000000..044e484fe9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart @@ -0,0 +1,78 @@ +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_action_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +enum BlockActionBottomSheetType { + delete, + duplicate, + insertAbove, + insertBelow, +} + +// Only works on mobile. +class BlockActionBottomSheet extends StatelessWidget { + const BlockActionBottomSheet({ + super.key, + required this.onAction, + this.extendActionWidgets = const [], + }); + + final void Function(BlockActionBottomSheetType layout) onAction; + final List extendActionWidgets; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // insert above, insert below + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.arrow_up_s, + text: LocaleKeys.button_insertAbove.tr(), + onTap: () => onAction(BlockActionBottomSheetType.insertAbove), + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.arrow_down_s, + text: LocaleKeys.button_insertBelow.tr(), + onTap: () => onAction(BlockActionBottomSheetType.insertBelow), + ), + ), + ], + ), + const VSpace(8), + + // duplicate, delete + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_duplicate_m, + text: LocaleKeys.button_duplicate.tr(), + onTap: () => onAction(BlockActionBottomSheetType.duplicate), + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_delete_m, + text: LocaleKeys.button_delete.tr(), + onTap: () => onAction(BlockActionBottomSheetType.delete), + ), + ), + ], + ), + const VSpace(8), + + ...extendActionWidgets, + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 4317f3afa2..d1dd3527c3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -121,31 +121,31 @@ class MobileViewBottomSheetBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // undo, redo - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_undo_m, - text: LocaleKeys.toolbar_undo.tr(), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.undo, - ), - ), - ), - const HSpace(8), - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_redo_m, - text: LocaleKeys.toolbar_redo.tr(), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.redo, - ), - ), - ), - ], - ), - const VSpace(8), + // Row( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Expanded( + // child: BottomSheetActionWidget( + // svg: FlowySvgs.m_undo_m, + // text: LocaleKeys.toolbar_undo.tr(), + // onTap: () => onAction( + // MobileViewBottomSheetBodyAction.undo, + // ), + // ), + // ), + // const HSpace(8), + // Expanded( + // child: BottomSheetActionWidget( + // svg: FlowySvgs.m_redo_m, + // text: LocaleKeys.toolbar_redo.tr(), + // onTap: () => onAction( + // MobileViewBottomSheetBodyAction.redo, + // ), + // ), + // ), + // ], + // ), + // const VSpace(8), // rename, duplicate Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 607f139d32..556a5d5a71 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -26,6 +26,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; +enum EditorNotificationType { + undo, + redo, +} + +class EditorNotification extends Notification { + const EditorNotification({ + required this.type, + }); + + EditorNotification.undo() : type = EditorNotificationType.undo; + EditorNotification.redo() : type = EditorNotificationType.redo; + + final EditorNotificationType type; +} + class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, @@ -95,7 +111,10 @@ class _DocumentPageState extends State { ); } else { editorState = documentBloc.editorState!; - return _buildEditorPage(context, state); + return _buildEditorPage( + context, + state, + ); } }, ), @@ -116,6 +135,7 @@ class _DocumentPageState extends State { ), header: _buildCoverAndIcon(context), ); + return Column( children: [ if (state.isDeleted) _buildBanner(context), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5d0394291e..d575aa29aa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -118,9 +118,7 @@ Map getEditorBuilderMap({ height: 28.0, ), MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (_) => const EdgeInsets.symmetric(vertical: 20), - ), + configuration: configuration, ), CodeBlockKeys.type: CodeBlockComponentBuilder( configuration: configuration.copyWith( 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 99f338bf80..bd774e4669 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -245,7 +245,7 @@ class _AppFlowyEditorPageState extends State { contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, - footer: const VSpace(200), + footer: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400), ), ); @@ -285,7 +285,11 @@ class _AppFlowyEditorPageState extends State { linkMobileToolbarItem, quoteMobileToolbarItem, dividerMobileToolbarItem, + imageMobileToolbarItem, + mathEquationMobileToolbarItem, codeMobileToolbarItem, + undoMobileToolbarItem, + redoMobileToolbarItem, ], ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart new file mode 100644 index 0000000000..b5fab17694 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -0,0 +1,107 @@ +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_block_action_widget.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.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'; + +/// The ... button shows on the top right corner of a block. +/// +/// Default actions are: +/// - delete +/// - duplicate +/// - insert above +/// - insert below +/// +/// Only works on mobile. +class MobileBlockActionButtons extends StatelessWidget { + const MobileBlockActionButtons({ + super.key, + this.extendActionWidgets = const [], + required this.node, + required this.editorState, + required this.child, + }); + + final Node node; + final EditorState editorState; + final List extendActionWidgets; + final Widget child; + + @override + Widget build(BuildContext context) { + if (!PlatformExtension.isMobile) { + return child; + } + + const padding = 5.0; + return Stack( + children: [ + child, + Positioned( + top: padding, + right: padding, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.three_dots_s, + ), + width: 20.0, + onPressed: () => _showBottomSheet(context), + ), + ), + ], + ); + } + + void _showBottomSheet(BuildContext context) { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.document_plugins_action.tr(), + builder: (context) { + return BlockActionBottomSheet( + extendActionWidgets: extendActionWidgets, + onAction: (action) async { + context.pop(); + + final transaction = editorState.transaction; + switch (action) { + case BlockActionBottomSheetType.delete: + transaction.deleteNode(node); + break; + case BlockActionBottomSheetType.duplicate: + transaction.insertNode( + node.path.next, + node.copyWith(), + ); + break; + case BlockActionBottomSheetType.insertAbove: + case BlockActionBottomSheetType.insertBelow: + final path = action == BlockActionBottomSheetType.insertAbove + ? node.path + : node.path.next; + transaction + ..insertNode( + path, + paragraphNode(), + ) + ..afterSelection = Selection.collapsed( + Position( + path: path, + ), + ); + break; + default: + } + + if (transaction.operations.isNotEmpty) { + await editorState.apply(transaction); + } + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart index 27d0745093..a30cf4451f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart @@ -74,6 +74,12 @@ class ClipboardService { await ClipboardWriter.instance.write([item]); } + Future setPlainText(String text) async { + await ClipboardWriter.instance.write([ + DataWriterItem()..add(Formats.plainText(text)), + ]); + } + Future getData() async { final reader = await ClipboardReader.readClipboard(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart index 0e35371792..1112343de8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart @@ -1,7 +1,24 @@ +import 'dart:io'; + +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/actions/mobile_block_action_buttons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.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/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:provider/provider.dart'; +import 'package:string_validator/string_validator.dart'; const kImagePlaceholderKey = 'imagePlaceholderKey'; @@ -96,25 +113,30 @@ class CustomImageBlockComponentState extends State final height = attributes[ImageBlockKeys.height]?.toDouble(); final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey]; - Widget child = src.isEmpty - ? ImagePlaceholder( - key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, - node: node, - ) - : ResizableImage( - src: src, - width: width, - height: height, - editable: editorState.editable, - alignment: alignment, - onResize: (width) { - final transaction = editorState.transaction - ..updateNode(node, { - ImageBlockKeys.width: width, - }); - editorState.apply(transaction); - }, - ); + Widget child; + if (src.isEmpty) { + child = ImagePlaceholder( + key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, + node: node, + ); + } else if (!_checkIfURLIsValid(src)) { + child = const UnSupportImageWidget(); + } else { + child = ResizableImage( + src: src, + width: width, + height: height, + editable: editorState.editable, + alignment: alignment, + onResize: (width) { + final transaction = editorState.transaction + ..updateNode(node, { + ImageBlockKeys.width: width, + }); + editorState.apply(transaction); + }, + ); + } child = BlockSelectionContainer( node: node, @@ -139,40 +161,51 @@ class CustomImageBlockComponentState extends State ); } - if (widget.showMenu && widget.menuBuilder != null) { - child = MouseRegion( - onEnter: (_) => showActionsNotifier.value = true, - onExit: (_) { - if (!alwaysShowMenu) { - showActionsNotifier.value = false; - } - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, value, child) { - final url = node.attributes[ImageBlockKeys.url]; - return Stack( - children: [ - BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - child: child!, - ), - if (value && url.isNotEmpty == true) - widget.menuBuilder!( - widget.node, - this, - ), - ], - ); + // show a hover menu on desktop or web + if (PlatformExtension.isDesktopOrWeb) { + if (widget.showMenu && widget.menuBuilder != null) { + child = MouseRegion( + onEnter: (_) => showActionsNotifier.value = true, + onExit: (_) { + if (!alwaysShowMenu) { + showActionsNotifier.value = false; + } }, - child: child, - ), + hitTestBehavior: HitTestBehavior.opaque, + opaque: false, + child: ValueListenableBuilder( + valueListenable: showActionsNotifier, + builder: (context, value, child) { + final url = node.attributes[ImageBlockKeys.url]; + return Stack( + children: [ + BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, + child: child!, + ), + if (value && url.isNotEmpty == true) + widget.menuBuilder!( + widget.node, + this, + ), + ], + ); + }, + child: child, + ), + ); + } + } else { + // show a fixed menu on mobile + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + extendActionWidgets: _buildExtendActionWidgets(context), + child: child, ); } @@ -246,4 +279,89 @@ class CustomImageBlockComponentState extends State bool shiftWithBaseOffset = false, }) => _renderBox!.localToGlobal(offset); + + // only used on mobile platform + List _buildExtendActionWidgets(BuildContext context) { + final url = widget.node.attributes[ImageBlockKeys.url]; + if (!_checkIfURLIsValid(url)) { + return []; + } + + return [ + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.copy_s, + text: LocaleKeys.editor_copyLink.tr(), + onTap: () async { + context.pop(); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + ); + await getIt().setPlainText(url); + }, + ), + ), + const HSpace(8.0), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.image_placeholder_s, + text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(), + onTap: () async { + context.pop(); + Uint8List? bytes; + if (isURL(url)) { + // network image + final result = await get(Uri.parse(url)); + if (result.statusCode == 200) { + bytes = result.bodyBytes; + } + } else { + final file = File(url); + bytes = await file.readAsBytes(); + } + if (bytes != null) { + await ImageGallerySaver.saveImage(bytes); + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_successToAddImageToGallery + .tr(), + ); + } + } else { + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_failedToAddImageToGallery + .tr(), + ); + } + } + }, + ), + ), + ], + ), + const VSpace(8), + ]; + } + + bool _checkIfURLIsValid(dynamic url) { + if (url is! String) { + return false; + } + + if (url.isEmpty) { + return false; + } + + if (!isURL(url) && !File(url).existsSync()) { + return false; + } + + return true; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart index 5420d949db..04e33fbbc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart @@ -32,6 +32,7 @@ class _EmbedImageUrlWidgetState extends State { SizedBox( width: 160, child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, margin: const EdgeInsets.all(8.0), text: FlowyText( LocaleKeys.document_imageBlock_embedLink_label.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index d77b864d57..bbc17f472d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; @@ -15,6 +16,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as p; import 'package:string_validator/string_validator.dart'; @@ -37,65 +39,114 @@ class ImagePlaceholderState extends State { @override Widget build(BuildContext context) { - return AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, + final Widget child = DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), ), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (context) { - return UploadImageMenu( - onSelectedLocalImage: (path) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertLocalImage(path); - }); - }, - onSelectedAIImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertAIImage(url); - }); - }, - onSelectedNetworkImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertNetworkImage(url); - }); - }, - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + child: FlowyHover( + style: HoverStyle( borderRadius: BorderRadius.circular(4), ), - child: FlowyHover( - style: HoverStyle( - borderRadius: BorderRadius.circular(4), - ), - child: SizedBox( - height: 48, - child: Row( - children: [ - const HSpace(10), - const FlowySvg( - FlowySvgs.image_placeholder_s, - size: Size.square(24), - ), - const HSpace(10), - FlowyText( - LocaleKeys.document_plugins_image_addAnImage.tr(), - ), - ], - ), + child: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(24), + ), + const HSpace(10), + FlowyText( + LocaleKeys.document_plugins_image_addAnImage.tr(), + ), + ], ), ), ), ); + + if (PlatformExtension.isDesktopOrWeb) { + return AppFlowyPopover( + controller: controller, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: const BoxConstraints( + maxWidth: 540, + maxHeight: 360, + minHeight: 80, + ), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (context) { + return UploadImageMenu( + onSelectedLocalImage: (path) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertLocalImage(path); + }); + }, + onSelectedAIImage: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertAIImage(url); + }); + }, + onSelectedNetworkImage: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertNetworkImage(url); + }); + }, + ); + }, + child: child, + ); + } else { + return GestureDetector( + onTap: () { + showUploadImageMenu(); + }, + child: child, + ); + } + } + + void showUploadImageMenu() { + if (PlatformExtension.isDesktopOrWeb) { + controller.show(); + } else { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.editor_image.tr(), + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + supportTypes: const [ + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + await insertLocalImage(path); + }, + onSelectedAIImage: (url) async { + context.pop(); + await insertAIImage(url); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + await insertNetworkImage(url); + }, + ), + ); + }, + ); + } } Future insertLocalImage(String? url) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart new file mode 100644 index 0000000000..10a756c02f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart @@ -0,0 +1,17 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final imageMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_toolbar_imae_lg), + actionHandler: (editorState, selection) async { + final imagePlaceholderKey = GlobalKey(); + await editorState.insertEmptyImageBlock(imagePlaceholderKey); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + imagePlaceholderKey.currentState?.showUploadImageMenu(); + }); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart new file mode 100644 index 0000000000..3403a1ff31 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +class UnSupportImageWidget extends StatelessWidget { + const UnSupportImageWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyHover( + style: HoverStyle( + borderRadius: BorderRadius.circular(4), + ), + child: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(24), + ), + const HSpace(10), + FlowyText( + LocaleKeys.document_imageBlock_unableToLoadImage.tr(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart index 40d1daf072..71c95ac445 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart @@ -1,10 +1,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; class UploadImageFileWidget extends StatelessWidget { const UploadImageFileWidget({ @@ -19,31 +21,34 @@ class UploadImageFileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyHover( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTapDown: (_) async { - final result = await getIt().pickFiles( - dialogTitle: '', - allowMultiple: false, - type: FileType.image, - allowedExtensions: allowedExtensions, - ); - onPickFile(result?.files.firstOrNull?.path); - }, - child: Container( + child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, + text: Container( + margin: const EdgeInsets.all(4.0), alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 8.0), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.surfaceVariant, - width: 1.0, - ), - ), child: FlowyText( LocaleKeys.document_imageBlock_upload_placeholder.tr(), ), ), + onTap: _uploadImage, ), ); } + + Future _uploadImage() async { + if (PlatformExtension.isDesktopOrWeb) { + // on desktop, the users can pick a image file from folder + final result = await getIt().pickFiles( + dialogTitle: '', + allowMultiple: false, + type: FileType.image, + allowedExtensions: allowedExtensions, + ); + onPickFile(result?.files.firstOrNull?.path); + } else { + // on mobile, the users can pick a image file from camera or image library + final result = await ImagePicker().pickImage(source: ImageSource.gallery); + onPickFile(result?.path); + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 0b8eed1f98..e7a9bfc7de 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stab import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -39,19 +40,21 @@ class UploadImageMenu extends StatefulWidget { required this.onSelectedLocalImage, required this.onSelectedAIImage, required this.onSelectedNetworkImage, + this.supportTypes = UploadImageType.values, }); final void Function(String? path) onSelectedLocalImage; final void Function(String url) onSelectedAIImage; final void Function(String url) onSelectedNetworkImage; + final List supportTypes; @override State createState() => _UploadImageMenuState(); } class _UploadImageMenuState extends State { + late final List values; int currentTabIndex = 0; - List values = UploadImageType.values; bool supportOpenAI = false; bool supportStabilityAI = false; @@ -59,6 +62,7 @@ class _UploadImageMenuState extends State { void initState() { super.initState(); + values = widget.supportTypes; UserBackendService.getCurrentUserProfile().then( (value) { final supportOpenAI = value.fold( @@ -97,15 +101,16 @@ class _UploadImageMenuState extends State { Theme.of(context).colorScheme.secondary, ), padding: EdgeInsets.zero, - // splashBorderRadius: BorderRadius.circular(4), tabs: values .map( (e) => FlowyHover( style: const HoverStyle(borderRadius: BorderRadius.zero), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, + padding: EdgeInsets.only( + left: 12.0, + right: 12.0, + bottom: 8.0, + top: PlatformExtension.isMobile ? 0 : 8.0, ), child: FlowyText(e.description), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index bb17ff1551..a3071ccef5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -1,7 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.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/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.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'; @@ -44,7 +46,7 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node( final mathEquationState = editorState.getNodeAtPath(path)?.key.currentState; if (mathEquationState != null && - mathEquationState is _MathEquationBlockComponentWidgetState) { + mathEquationState is MathEquationBlockComponentWidgetState) { mathEquationState.showEditingDialog(); } }); @@ -89,10 +91,10 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { @override State createState() => - _MathEquationBlockComponentWidgetState(); + MathEquationBlockComponentWidgetState(); } -class _MathEquationBlockComponentWidgetState +class MathEquationBlockComponentWidgetState extends State with BlockComponentConfigurable { @override @@ -112,35 +114,34 @@ class _MathEquationBlockComponentWidgetState return InkWell( onHover: (value) => setState(() => isHover = value), onTap: showEditingDialog, - child: _buildMathEquation(context), + child: _build(context), ); } - Widget _buildMathEquation(BuildContext context) { + Widget _build(BuildContext context) { Widget child = Container( - width: double.infinity, - constraints: const BoxConstraints(minHeight: 50), - padding: padding, + constraints: const BoxConstraints(minHeight: 52), decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - color: isHover || formula.isEmpty - ? Theme.of(context).colorScheme.tertiaryContainer - : Colors.transparent, + color: formula.isNotEmpty + ? Colors.transparent + : Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), ), - child: Center( + child: FlowyHover( + style: HoverStyle( + borderRadius: BorderRadius.circular(4), + ), child: formula.isEmpty - ? FlowyText.medium( - LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), - fontSize: 16, - ) - : Math.tex( - formula, - textStyle: const TextStyle(fontSize: 20), - mathStyle: MathStyle.display, - ), + ? _buildPlaceholderWidget(context) + : _buildMathEquation(context), ), ); + child = Padding( + padding: padding, + child: child, + ); + if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: node, @@ -149,9 +150,43 @@ class _MathEquationBlockComponentWidgetState ); } + if (PlatformExtension.isMobile) { + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + child: child, + ); + } + return child; } + Widget _buildPlaceholderWidget(BuildContext context) { + return SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const Icon(Icons.text_fields_outlined), + const HSpace(10), + FlowyText( + LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), + ), + ], + ), + ); + } + + Widget _buildMathEquation(BuildContext context) { + return Center( + child: Math.tex( + formula, + textStyle: const TextStyle(fontSize: 20), + mathStyle: MathStyle.display, + ), + ); + } + void showEditingDialog() { showDialog( context: context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart new file mode 100644 index 0000000000..5e7f7dcc77 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final mathEquationMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)), + actionHandler: (editorState, selection) async { + if (!selection.isCollapsed) { + return; + } + final path = selection.start.path; + final node = editorState.getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final transaction = editorState.transaction; + final insertedNode = mathEquationNode(); + + if (delta.isEmpty) { + transaction + ..insertNode(path, insertedNode) + ..deleteNode(node); + } else { + transaction.insertNode( + path.next, + insertedNode, + ); + } + + await editorState.apply(transaction); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final mathEquationState = + editorState.getNodeAtPath(path)?.key.currentState; + if (mathEquationState != null && + mathEquationState is MathEquationBlockComponentWidgetState) { + mathEquationState.showEditingDialog(); + } + }); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 37d38be4d8..49b212c40e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -21,9 +21,11 @@ export 'header/custom_cover_picker.dart'; export 'header/document_header_node_widget.dart'; export 'image/image_menu.dart'; export 'image/image_selection_menu.dart'; +export 'image/mobile_image_toolbar_item.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'math_equation/math_equation_block_component.dart'; +export 'math_equation/mobile_math_eqaution_toolbar_item.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_toolbar_item.dart'; @@ -33,3 +35,5 @@ export 'table/table_menu.dart'; export 'table/table_option_action.dart'; export 'toggle/toggle_block_component.dart'; export 'toggle/toggle_block_shortcut_event.dart'; +export 'undo_redo/redo_mobile_toolbar_item.dart'; +export 'undo_redo/undo_mobile_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart new file mode 100644 index 0000000000..99b29f9b42 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart @@ -0,0 +1,9 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final redoMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_redo_m), + actionHandler: (editorState, selection) async { + editorState.undoManager.redo(); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart new file mode 100644 index 0000000000..cf132c1486 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart @@ -0,0 +1,9 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final undoMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_undo_m), + actionHandler: (editorState, selection) async { + editorState.undoManager.undo(); + }, +); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 854cb8e6c9..3cb102db42 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -22,6 +22,7 @@ class FlowyMessageToast extends StatelessWidget { child: FlowyText.medium( message, fontSize: FontSizes.s16, + maxLines: 3, ), ), ); @@ -32,12 +33,17 @@ void initToastWithContext(BuildContext context) { getIt().init(context); } -void showMessageToast(String message) { +void showMessageToast( + String message, { + BuildContext? context, + ToastGravity gravity = ToastGravity.BOTTOM, +}) { final child = FlowyMessageToast(message: message); - - getIt().showToast( + final toast = context == null ? getIt() : FToast() + ..init(context!); + toast.showToast( child: child, - gravity: ToastGravity.BOTTOM, + gravity: gravity, toastDuration: const Duration(seconds: 3), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart index 8039ea1e25..e8991397a9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart @@ -1,8 +1,8 @@ +import 'package:file_picker/file_picker.dart'; + export 'package:file_picker/file_picker.dart' show FileType, FilePickerStatus, PlatformFile; -import 'package:file_picker/file_picker.dart'; - class FilePickerResult { const FilePickerResult(this.files); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 906058e6d1..c9e21e273d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -24,6 +26,7 @@ class FlowyButton extends StatelessWidget { final Size? leftIconSize; final bool expandText; final MainAxisAlignment mainAxisAlignment; + final bool showDefaultBoxDecorationOnMobile; const FlowyButton({ Key? key, @@ -44,6 +47,7 @@ class FlowyButton extends StatelessWidget { this.leftIconSize = const Size.square(16), this.expandText = true, this.mainAxisAlignment = MainAxisAlignment.center, + this.showDefaultBoxDecorationOnMobile = false, }) : super(key: key); @override @@ -65,12 +69,12 @@ class FlowyButton extends StatelessWidget { ), onHover: disable ? null : onHover, isSelected: () => isSelected, - builder: (context, onHover) => _render(), + builder: (context, onHover) => _render(context), ), ); } - Widget _render() { + Widget _render(BuildContext context) { List children = List.empty(growable: true); if (leftIcon != null) { @@ -105,6 +109,16 @@ class FlowyButton extends StatelessWidget { child = IntrinsicWidth(child: child); } + final decoration = this.decoration ?? + (showDefaultBoxDecorationOnMobile && + (Platform.isIOS || Platform.isAndroid) + ? BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceVariant, + width: 1.0, + )) + : null); + return Container( decoration: decoration, child: Padding( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index bede73fec4..9efa52c66d 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,12 @@ packages: appflowy_editor: dependency: "direct main" description: - name: appflowy_editor - sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad - url: "https://pub.dev" - source: hosted - version: "1.5.1" + path: "." + ref: a47fc6f + resolved-ref: a47fc6fc712b06991f578ae2ab314cbe23034e96 + url: "https://github.com/AppFlowy-IO/appflowy-editor.git" + source: git + version: "1.5.2" appflowy_popover: dependency: "direct main" description: @@ -273,6 +274,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + url: "https://pub.dev" + source: hosted + version: "0.3.3+6" crypto: dependency: transitive description: @@ -442,6 +451,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: "direct main" description: @@ -740,6 +781,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" + url: "https://pub.dev" + source: hosted + version: "0.8.8+4" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 86cfd59a85..862d1cbea9 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -44,7 +44,10 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: 1a329c2 - appflowy_editor: ^1.5.1 + appflowy_editor: + git: + url: https://github.com/AppFlowy-IO/appflowy-editor.git + ref: a47fc6f appflowy_popover: path: packages/appflowy_popover @@ -118,6 +121,8 @@ dependencies: local_notifier: ^0.1.5 app_links: ^3.4.1 flutter_slidable: ^3.0.0 + image_picker: ^1.0.4 + image_gallery_saver: ^2.0.3 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg b/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg new file mode 100644 index 0000000000..e694a1bc7c --- /dev/null +++ b/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b38493cf76..786a6048db 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -214,7 +214,8 @@ "tryAgain": "Try again", "discard": "Discard", "replace": "Replace", - "insertBelow": "Insert Below", + "insertBelow": "Insert below", + "insertAbove": "Insert above", "upload": "Upload", "edit": "Edit", "delete": "Delete", @@ -639,7 +640,7 @@ "alertDialogConfirmation": "Are you sure, you want to continue?" }, "mathEquation": { - "addMathEquation": "Add Math Equation", + "addMathEquation": "Add a TeX equation", "editMathEquation": "Edit Math Equation" }, "optionAction": { @@ -676,7 +677,8 @@ "copy": "Copy", "cut": "Cut", "paste": "Paste" - } + }, + "action": "Actions" }, "textBlock": { "placeholder": "Type '/' for commands" @@ -715,7 +717,11 @@ }, "searchForAnImage": "Search for an image", "pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", - "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page" + "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page", + "saveImageToGallery": "Save image", + "failedToAddImageToGallery": "Failed to add image to gallery", + "successToAddImageToGallery": "Image added to gallery successfully", + "unableToLoadImage": "Unable to load image" }, "codeBlock": { "language": {