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
This commit is contained in:
Lucas.Xu 2023-11-07 15:24:32 +08:00 committed by GitHub
parent 3e6529aeb8
commit 8116ea1dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 928 additions and 201 deletions

View File

@ -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

View File

@ -2,8 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>AppFlowy requires access to the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library.</string>
<string>AppFlowy requires access to the photo library.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>

View File

@ -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,

View File

@ -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<MobileViewPage> {
context.read<FavoriteBloc>().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();
}
},

View File

@ -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),

View File

@ -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<Widget> 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,
],
);
}
}

View File

@ -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(

View File

@ -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<DocumentPage> {
);
} else {
editorState = documentBloc.editorState!;
return _buildEditorPage(context, state);
return _buildEditorPage(
context,
state,
);
}
},
),
@ -116,6 +135,7 @@ class _DocumentPageState extends State<DocumentPage> {
),
header: _buildCoverAndIcon(context),
);
return Column(
children: [
if (state.isDeleted) _buildBanner(context),

View File

@ -118,9 +118,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
height: 28.0,
),
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
),
configuration: configuration,
),
CodeBlockKeys.type: CodeBlockComponentBuilder(
configuration: configuration.copyWith(

View File

@ -245,7 +245,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
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<AppFlowyEditorPage> {
linkMobileToolbarItem,
quoteMobileToolbarItem,
dividerMobileToolbarItem,
imageMobileToolbarItem,
mathEquationMobileToolbarItem,
codeMobileToolbarItem,
undoMobileToolbarItem,
redoMobileToolbarItem,
],
),
],

View File

@ -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<Widget> 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);
}
},
);
},
);
}
}

View File

@ -74,6 +74,12 @@ class ClipboardService {
await ClipboardWriter.instance.write([item]);
}
Future<void> setPlainText(String text) async {
await ClipboardWriter.instance.write([
DataWriterItem()..add(Formats.plainText(text)),
]);
}
Future<ClipboardServiceData> getData() async {
final reader = await ClipboardReader.readClipboard();

View File

@ -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<CustomImageBlockComponent>
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<CustomImageBlockComponent>
);
}
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<bool>(
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<bool>(
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<CustomImageBlockComponent>
bool shiftWithBaseOffset = false,
}) =>
_renderBox!.localToGlobal(offset);
// only used on mobile platform
List<Widget> _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<ClipboardService>().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;
}
}

View File

@ -32,6 +32,7 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
SizedBox(
width: 160,
child: FlowyButton(
showDefaultBoxDecorationOnMobile: true,
margin: const EdgeInsets.all(8.0),
text: FlowyText(
LocaleKeys.document_imageBlock_embedLink_label.tr(),

View File

@ -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<ImagePlaceholder> {
@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<void> insertLocalImage(String? url) async {

View File

@ -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<ImagePlaceholderState>();
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
imagePlaceholderKey.currentState?.showUploadImageMenu();
});
},
);

View File

@ -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(),
),
],
),
),
),
);
}
}

View File

@ -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<FilePickerService>().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<void> _uploadImage() async {
if (PlatformExtension.isDesktopOrWeb) {
// on desktop, the users can pick a image file from folder
final result = await getIt<FilePickerService>().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);
}
}
}

View File

@ -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<UploadImageType> supportTypes;
@override
State<UploadImageMenu> createState() => _UploadImageMenuState();
}
class _UploadImageMenuState extends State<UploadImageMenu> {
late final List<UploadImageType> values;
int currentTabIndex = 0;
List<UploadImageType> values = UploadImageType.values;
bool supportOpenAI = false;
bool supportStabilityAI = false;
@ -59,6 +62,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
void initState() {
super.initState();
values = widget.supportTypes;
UserBackendService.getCurrentUserProfile().then(
(value) {
final supportOpenAI = value.fold(
@ -97,15 +101,16 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
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),
),

View File

@ -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<MathEquationBlockComponentWidget> createState() =>
_MathEquationBlockComponentWidgetState();
MathEquationBlockComponentWidgetState();
}
class _MathEquationBlockComponentWidgetState
class MathEquationBlockComponentWidgetState
extends State<MathEquationBlockComponentWidget>
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,

View File

@ -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();
}
});
},
);

View File

@ -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';

View File

@ -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();
},
);

View File

@ -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();
},
);

View File

@ -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<FToast>().init(context);
}
void showMessageToast(String message) {
void showMessageToast(
String message, {
BuildContext? context,
ToastGravity gravity = ToastGravity.BOTTOM,
}) {
final child = FlowyMessageToast(message: message);
getIt<FToast>().showToast(
final toast = context == null ? getIt<FToast>() : FToast()
..init(context!);
toast.showToast(
child: child,
gravity: ToastGravity.BOTTOM,
gravity: gravity,
toastDuration: const Duration(seconds: 3),
);
}

View File

@ -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);

View File

@ -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<Widget> 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(

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.49805 2.99072C5.28905 2.99072 3.49805 4.78172 3.49805 6.99072V15.9907V16.9907C3.49805 19.1997 5.28905 20.9907 7.49805 20.9907H17.498C19.707 20.9907 21.498 19.1997 21.498 16.9907V15.9907V6.99072C21.498 4.78172 19.707 2.99072 17.498 2.99072H7.49805ZM7.49805 4.99072H17.498C18.603 4.99072 19.498 5.88572 19.498 6.99072L19.502 13.1867C18.724 12.4647 17.9311 12.0127 17.0601 11.9907C17.0291 11.9897 17.03 11.9907 16.998 11.9907C15.861 11.9907 14.597 12.8647 13.748 13.9597C13.531 13.4697 13.3121 12.9857 13.0601 12.5217C11.8801 10.3427 10.591 8.99372 8.99805 8.99072C7.64405 8.98772 6.48205 10.0947 5.49005 11.6107L5.49805 6.99072C5.49805 5.88572 6.39305 4.99072 7.49805 4.99072ZM16.498 6.99072C15.946 6.99072 15.498 7.43872 15.498 7.99072C15.498 8.54272 15.946 8.99072 16.498 8.99072C17.05 8.99072 17.498 8.54272 17.498 7.99072C17.498 7.43872 17.05 6.99072 16.498 6.99072ZM8.99805 10.9907C9.58005 10.9917 10.4641 11.8977 11.3101 13.4597C11.6461 14.0777 11.947 14.7587 12.217 15.4287C12.379 15.8287 12.5041 16.1347 12.5601 16.3027C12.8331 17.1197 13.941 17.2357 14.373 16.4907C14.416 16.4167 14.494 16.2747 14.623 16.0847C14.839 15.7647 15.085 15.4407 15.342 15.1467C15.984 14.4107 16.603 13.9907 16.998 13.9907C17.397 14.0007 18.0131 14.4197 18.6541 15.1467C18.9141 15.4417 19.154 15.7647 19.373 16.0847C19.442 16.1847 19.452 16.2317 19.498 16.3027V16.9907C19.498 18.0957 18.603 18.9907 17.498 18.9907H7.49805C6.39305 18.9907 5.49805 18.0957 5.49805 16.9907V16.1467C5.56105 15.9687 5.64505 15.7577 5.77905 15.4287C6.05205 14.7587 6.34804 14.0787 6.68604 13.4597C7.53904 11.8947 8.41705 10.9897 8.99805 10.9907Z" fill="#676666"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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": {