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
31 changed files with 928 additions and 201 deletions

View File

@ -48,6 +48,10 @@ PODS:
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
- Toast - Toast
- image_gallery_saver (2.0.2):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1): - integration_test (0.0.1):
- Flutter - Flutter
- irondash_engine_context (0.0.1): - irondash_engine_context (0.0.1):
@ -86,6 +90,8 @@ DEPENDENCIES:
- flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`) - flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - 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`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -123,6 +129,10 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :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: integration_test:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context: irondash_engine_context:
@ -155,6 +165,8 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670 integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSCameraUsageDescription</key>
<string>AppFlowy requires access to the camera.</string>
<key>NSPhotoLibraryUsageDescription</key> <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> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

View File

@ -34,6 +34,7 @@ ThemeData getMobileThemeData(
//Snack bar //Snack bar
surface: Colors.white, surface: Colors.white,
onSurface: _onSurfaceColor, // text/body color onSurface: _onSurfaceColor, // text/body color
surfaceVariant: const Color.fromARGB(255, 216, 216, 216),
) )
: ColorScheme( : ColorScheme(
brightness: brightness, brightness: brightness,
@ -223,6 +224,7 @@ ThemeData getMobileThemeData(
), ),
), ),
colorScheme: mobileColorTheme, colorScheme: mobileColorTheme,
indicatorColor: Colors.blue,
extensions: [ extensions: [
AFThemeExtension( AFThemeExtension(
warning: theme.yellow, warning: theme.yellow,

View File

@ -1,7 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart'; 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/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.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)); context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break; break;
case MobileViewBottomSheetBodyAction.undo: case MobileViewBottomSheetBodyAction.undo:
context.dispatchNotification(
const EditorNotification(type: EditorNotificationType.redo),
);
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo: case MobileViewBottomSheetBodyAction.redo:
context.pop();
context.dispatchNotification(EditorNotification.redo());
break;
case MobileViewBottomSheetBodyAction.helpCenter: case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented // unimplemented
context.pop(); context.pop();
break; break;
case MobileViewBottomSheetBodyAction.rename: case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError(); throw UnimplementedError();
} }
}, },

View File

@ -24,6 +24,7 @@ class BottomSheetActionWidget extends StatelessWidget {
icon: FlowySvg( icon: FlowySvg(
svg, svg,
size: const Size.square(22.0), size: const Size.square(22.0),
blendMode: BlendMode.dst,
color: iconColor, color: iconColor,
), ),
label: Text(text), 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, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// undo, redo // undo, redo
Row( // Row(
mainAxisSize: MainAxisSize.max, // mainAxisSize: MainAxisSize.max,
children: [ // children: [
Expanded( // Expanded(
child: BottomSheetActionWidget( // child: BottomSheetActionWidget(
svg: FlowySvgs.m_undo_m, // svg: FlowySvgs.m_undo_m,
text: LocaleKeys.toolbar_undo.tr(), // text: LocaleKeys.toolbar_undo.tr(),
onTap: () => onAction( // onTap: () => onAction(
MobileViewBottomSheetBodyAction.undo, // MobileViewBottomSheetBodyAction.undo,
), // ),
), // ),
), // ),
const HSpace(8), // const HSpace(8),
Expanded( // Expanded(
child: BottomSheetActionWidget( // child: BottomSheetActionWidget(
svg: FlowySvgs.m_redo_m, // svg: FlowySvgs.m_redo_m,
text: LocaleKeys.toolbar_redo.tr(), // text: LocaleKeys.toolbar_redo.tr(),
onTap: () => onAction( // onTap: () => onAction(
MobileViewBottomSheetBodyAction.redo, // MobileViewBottomSheetBodyAction.redo,
), // ),
), // ),
), // ),
], // ],
), // ),
const VSpace(8), // const VSpace(8),
// rename, duplicate // rename, duplicate
Row( Row(

View File

@ -26,6 +26,22 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:path/path.dart' as p; 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 { class DocumentPage extends StatefulWidget {
const DocumentPage({ const DocumentPage({
super.key, super.key,
@ -95,7 +111,10 @@ class _DocumentPageState extends State<DocumentPage> {
); );
} else { } else {
editorState = documentBloc.editorState!; editorState = documentBloc.editorState!;
return _buildEditorPage(context, state); return _buildEditorPage(
context,
state,
);
} }
}, },
), ),
@ -116,6 +135,7 @@ class _DocumentPageState extends State<DocumentPage> {
), ),
header: _buildCoverAndIcon(context), header: _buildCoverAndIcon(context),
); );
return Column( return Column(
children: [ children: [
if (state.isDeleted) _buildBanner(context), if (state.isDeleted) _buildBanner(context),

View File

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

View File

@ -245,7 +245,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
contextMenuItems: customContextMenuItems, contextMenuItems: customContextMenuItems,
// customize the header and footer. // customize the header and footer.
header: widget.header, header: widget.header,
footer: const VSpace(200), footer: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400),
), ),
); );
@ -285,7 +285,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
linkMobileToolbarItem, linkMobileToolbarItem,
quoteMobileToolbarItem, quoteMobileToolbarItem,
dividerMobileToolbarItem, dividerMobileToolbarItem,
imageMobileToolbarItem,
mathEquationMobileToolbarItem,
codeMobileToolbarItem, 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]); await ClipboardWriter.instance.write([item]);
} }
Future<void> setPlainText(String text) async {
await ClipboardWriter.instance.write([
DataWriterItem()..add(Formats.plainText(text)),
]);
}
Future<ClipboardServiceData> getData() async { Future<ClipboardServiceData> getData() async {
final reader = await ClipboardReader.readClipboard(); 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/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: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: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:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
const kImagePlaceholderKey = 'imagePlaceholderKey'; const kImagePlaceholderKey = 'imagePlaceholderKey';
@ -96,12 +113,16 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
final height = attributes[ImageBlockKeys.height]?.toDouble(); final height = attributes[ImageBlockKeys.height]?.toDouble();
final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey]; final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey];
Widget child = src.isEmpty Widget child;
? ImagePlaceholder( if (src.isEmpty) {
child = ImagePlaceholder(
key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,
node: node, node: node,
) );
: ResizableImage( } else if (!_checkIfURLIsValid(src)) {
child = const UnSupportImageWidget();
} else {
child = ResizableImage(
src: src, src: src,
width: width, width: width,
height: height, height: height,
@ -115,6 +136,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
editorState.apply(transaction); editorState.apply(transaction);
}, },
); );
}
child = BlockSelectionContainer( child = BlockSelectionContainer(
node: node, node: node,
@ -139,6 +161,8 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
); );
} }
// show a hover menu on desktop or web
if (PlatformExtension.isDesktopOrWeb) {
if (widget.showMenu && widget.menuBuilder != null) { if (widget.showMenu && widget.menuBuilder != null) {
child = MouseRegion( child = MouseRegion(
onEnter: (_) => showActionsNotifier.value = true, onEnter: (_) => showActionsNotifier.value = true,
@ -175,6 +199,15 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
), ),
); );
} }
} else {
// show a fixed menu on mobile
child = MobileBlockActionButtons(
node: node,
editorState: editorState,
extendActionWidgets: _buildExtendActionWidgets(context),
child: child,
);
}
return child; return child;
} }
@ -246,4 +279,89 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
bool shiftWithBaseOffset = false, bool shiftWithBaseOffset = false,
}) => }) =>
_renderBox!.localToGlobal(offset); _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( SizedBox(
width: 160, width: 160,
child: FlowyButton( child: FlowyButton(
showDefaultBoxDecorationOnMobile: true,
margin: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0),
text: FlowyText( text: FlowyText(
LocaleKeys.document_imageBlock_embedLink_label.tr(), 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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
@ -37,6 +39,35 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget child = 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_plugins_image_addAnImage.tr(),
),
],
),
),
),
);
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover( return AppFlowyPopover(
controller: controller, controller: controller,
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
@ -68,34 +99,54 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
}, },
); );
}, },
child: DecoratedBox( child: child,
decoration: BoxDecoration( );
color: Theme.of(context).colorScheme.surfaceVariant, } else {
borderRadius: BorderRadius.circular(4), return GestureDetector(
), onTap: () {
child: FlowyHover( showUploadImageMenu();
style: HoverStyle( },
borderRadius: BorderRadius.circular(4), child: child,
), );
child: SizedBox( }
height: 48, }
child: Row(
children: [ void showUploadImageMenu() {
const HSpace(10), if (PlatformExtension.isDesktopOrWeb) {
const FlowySvg( controller.show();
FlowySvgs.image_placeholder_s, } else {
size: Size.square(24), showFlowyMobileBottomSheet(
), context,
const HSpace(10), title: LocaleKeys.editor_image.tr(),
FlowyText( builder: (context) {
LocaleKeys.document_plugins_image_addAnImage.tr(), 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 { 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/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.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/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class UploadImageFileWidget extends StatelessWidget { class UploadImageFileWidget extends StatelessWidget {
const UploadImageFileWidget({ const UploadImageFileWidget({
@ -19,9 +21,23 @@ class UploadImageFileWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyHover( return FlowyHover(
child: GestureDetector( child: FlowyButton(
behavior: HitTestBehavior.translucent, showDefaultBoxDecorationOnMobile: true,
onTapDown: (_) async { text: Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
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( final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: '', dialogTitle: '',
allowMultiple: false, allowMultiple: false,
@ -29,21 +45,10 @@ class UploadImageFileWidget extends StatelessWidget {
allowedExtensions: allowedExtensions, allowedExtensions: allowedExtensions,
); );
onPickFile(result?.files.firstOrNull?.path); onPickFile(result?.files.firstOrNull?.path);
}, } else {
child: Container( // on mobile, the users can pick a image file from camera or image library
alignment: Alignment.center, final result = await ImagePicker().pickImage(source: ImageSource.gallery);
padding: const EdgeInsets.symmetric(vertical: 8.0), onPickFile(result?.path);
decoration: BoxDecoration( }
border: Border.all(
color: Theme.of(context).colorScheme.surfaceVariant,
width: 1.0,
),
),
child: FlowyText(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
),
),
),
);
} }
} }

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/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_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/user/application/user_service.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -39,19 +40,21 @@ class UploadImageMenu extends StatefulWidget {
required this.onSelectedLocalImage, required this.onSelectedLocalImage,
required this.onSelectedAIImage, required this.onSelectedAIImage,
required this.onSelectedNetworkImage, required this.onSelectedNetworkImage,
this.supportTypes = UploadImageType.values,
}); });
final void Function(String? path) onSelectedLocalImage; final void Function(String? path) onSelectedLocalImage;
final void Function(String url) onSelectedAIImage; final void Function(String url) onSelectedAIImage;
final void Function(String url) onSelectedNetworkImage; final void Function(String url) onSelectedNetworkImage;
final List<UploadImageType> supportTypes;
@override @override
State<UploadImageMenu> createState() => _UploadImageMenuState(); State<UploadImageMenu> createState() => _UploadImageMenuState();
} }
class _UploadImageMenuState extends State<UploadImageMenu> { class _UploadImageMenuState extends State<UploadImageMenu> {
late final List<UploadImageType> values;
int currentTabIndex = 0; int currentTabIndex = 0;
List<UploadImageType> values = UploadImageType.values;
bool supportOpenAI = false; bool supportOpenAI = false;
bool supportStabilityAI = false; bool supportStabilityAI = false;
@ -59,6 +62,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
void initState() { void initState() {
super.initState(); super.initState();
values = widget.supportTypes;
UserBackendService.getCurrentUserProfile().then( UserBackendService.getCurrentUserProfile().then(
(value) { (value) {
final supportOpenAI = value.fold( final supportOpenAI = value.fold(
@ -97,15 +101,16 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
Theme.of(context).colorScheme.secondary, Theme.of(context).colorScheme.secondary,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
// splashBorderRadius: BorderRadius.circular(4),
tabs: values tabs: values
.map( .map(
(e) => FlowyHover( (e) => FlowyHover(
style: const HoverStyle(borderRadius: BorderRadius.zero), style: const HoverStyle(borderRadius: BorderRadius.zero),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.only(
horizontal: 12.0, left: 12.0,
vertical: 8.0, right: 12.0,
bottom: 8.0,
top: PlatformExtension.isMobile ? 0 : 8.0,
), ),
child: FlowyText(e.description), child: FlowyText(e.description),
), ),

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart'; 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:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.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/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -44,7 +46,7 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
final mathEquationState = final mathEquationState =
editorState.getNodeAtPath(path)?.key.currentState; editorState.getNodeAtPath(path)?.key.currentState;
if (mathEquationState != null && if (mathEquationState != null &&
mathEquationState is _MathEquationBlockComponentWidgetState) { mathEquationState is MathEquationBlockComponentWidgetState) {
mathEquationState.showEditingDialog(); mathEquationState.showEditingDialog();
} }
}); });
@ -89,10 +91,10 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget {
@override @override
State<MathEquationBlockComponentWidget> createState() => State<MathEquationBlockComponentWidget> createState() =>
_MathEquationBlockComponentWidgetState(); MathEquationBlockComponentWidgetState();
} }
class _MathEquationBlockComponentWidgetState class MathEquationBlockComponentWidgetState
extends State<MathEquationBlockComponentWidget> extends State<MathEquationBlockComponentWidget>
with BlockComponentConfigurable { with BlockComponentConfigurable {
@override @override
@ -112,35 +114,34 @@ class _MathEquationBlockComponentWidgetState
return InkWell( return InkWell(
onHover: (value) => setState(() => isHover = value), onHover: (value) => setState(() => isHover = value),
onTap: showEditingDialog, onTap: showEditingDialog,
child: _buildMathEquation(context), child: _build(context),
); );
} }
Widget _buildMathEquation(BuildContext context) { Widget _build(BuildContext context) {
Widget child = Container( Widget child = Container(
width: double.infinity, constraints: const BoxConstraints(minHeight: 52),
constraints: const BoxConstraints(minHeight: 50),
padding: padding,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)), color: formula.isNotEmpty
color: isHover || formula.isEmpty ? Colors.transparent
? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.surfaceVariant,
: Colors.transparent, borderRadius: BorderRadius.circular(4),
),
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
), ),
child: Center(
child: formula.isEmpty child: formula.isEmpty
? FlowyText.medium( ? _buildPlaceholderWidget(context)
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), : _buildMathEquation(context),
fontSize: 16,
)
: Math.tex(
formula,
textStyle: const TextStyle(fontSize: 20),
mathStyle: MathStyle.display,
),
), ),
); );
child = Padding(
padding: padding,
child: child,
);
if (widget.showActions && widget.actionBuilder != null) { if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper( child = BlockComponentActionWrapper(
node: node, node: node,
@ -149,9 +150,43 @@ class _MathEquationBlockComponentWidgetState
); );
} }
if (PlatformExtension.isMobile) {
child = MobileBlockActionButtons(
node: node,
editorState: editorState,
child: child,
);
}
return 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() { void showEditingDialog() {
showDialog( showDialog(
context: context, 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 'header/document_header_node_widget.dart';
export 'image/image_menu.dart'; export 'image/image_menu.dart';
export 'image/image_selection_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.dart';
export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
export 'math_equation/math_equation_block_component.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/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.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 'table/table_option_action.dart';
export 'toggle/toggle_block_component.dart'; export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.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( child: FlowyText.medium(
message, message,
fontSize: FontSizes.s16, fontSize: FontSizes.s16,
maxLines: 3,
), ),
), ),
); );
@ -32,12 +33,17 @@ void initToastWithContext(BuildContext context) {
getIt<FToast>().init(context); getIt<FToast>().init(context);
} }
void showMessageToast(String message) { void showMessageToast(
String message, {
BuildContext? context,
ToastGravity gravity = ToastGravity.BOTTOM,
}) {
final child = FlowyMessageToast(message: message); final child = FlowyMessageToast(message: message);
final toast = context == null ? getIt<FToast>() : FToast()
getIt<FToast>().showToast( ..init(context!);
toast.showToast(
child: child, child: child,
gravity: ToastGravity.BOTTOM, gravity: gravity,
toastDuration: const Duration(seconds: 3), 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' export 'package:file_picker/file_picker.dart'
show FileType, FilePickerStatus, PlatformFile; show FileType, FilePickerStatus, PlatformFile;
import 'package:file_picker/file_picker.dart';
class FilePickerResult { class FilePickerResult {
const FilePickerResult(this.files); const FilePickerResult(this.files);

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -24,6 +26,7 @@ class FlowyButton extends StatelessWidget {
final Size? leftIconSize; final Size? leftIconSize;
final bool expandText; final bool expandText;
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
final bool showDefaultBoxDecorationOnMobile;
const FlowyButton({ const FlowyButton({
Key? key, Key? key,
@ -44,6 +47,7 @@ class FlowyButton extends StatelessWidget {
this.leftIconSize = const Size.square(16), this.leftIconSize = const Size.square(16),
this.expandText = true, this.expandText = true,
this.mainAxisAlignment = MainAxisAlignment.center, this.mainAxisAlignment = MainAxisAlignment.center,
this.showDefaultBoxDecorationOnMobile = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -65,12 +69,12 @@ class FlowyButton extends StatelessWidget {
), ),
onHover: disable ? null : onHover, onHover: disable ? null : onHover,
isSelected: () => isSelected, isSelected: () => isSelected,
builder: (context, onHover) => _render(), builder: (context, onHover) => _render(context),
), ),
); );
} }
Widget _render() { Widget _render(BuildContext context) {
List<Widget> children = List.empty(growable: true); List<Widget> children = List.empty(growable: true);
if (leftIcon != null) { if (leftIcon != null) {
@ -105,6 +109,16 @@ class FlowyButton extends StatelessWidget {
child = IntrinsicWidth(child: child); 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( return Container(
decoration: decoration, decoration: decoration,
child: Padding( child: Padding(

View File

@ -53,11 +53,12 @@ packages:
appflowy_editor: appflowy_editor:
dependency: "direct main" dependency: "direct main"
description: description:
name: appflowy_editor path: "."
sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad ref: a47fc6f
url: "https://pub.dev" resolved-ref: a47fc6fc712b06991f578ae2ab314cbe23034e96
source: hosted url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
version: "1.5.1" source: git
version: "1.5.2"
appflowy_popover: appflowy_popover:
dependency: "direct main" dependency: "direct main"
description: description:
@ -273,6 +274,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.3" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -442,6 +451,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.1" 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: fixnum:
dependency: "direct main" dependency: "direct main"
description: description:
@ -740,6 +781,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" 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: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View File

@ -44,7 +44,10 @@ dependencies:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-board.git url: https://github.com/AppFlowy-IO/appflowy-board.git
ref: 1a329c2 ref: 1a329c2
appflowy_editor: ^1.5.1 appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: a47fc6f
appflowy_popover: appflowy_popover:
path: packages/appflowy_popover path: packages/appflowy_popover
@ -118,6 +121,8 @@ dependencies:
local_notifier: ^0.1.5 local_notifier: ^0.1.5
app_links: ^3.4.1 app_links: ^3.4.1
flutter_slidable: ^3.0.0 flutter_slidable: ^3.0.0
image_picker: ^1.0.4
image_gallery_saver: ^2.0.3
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 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", "tryAgain": "Try again",
"discard": "Discard", "discard": "Discard",
"replace": "Replace", "replace": "Replace",
"insertBelow": "Insert Below", "insertBelow": "Insert below",
"insertAbove": "Insert above",
"upload": "Upload", "upload": "Upload",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
@ -639,7 +640,7 @@
"alertDialogConfirmation": "Are you sure, you want to continue?" "alertDialogConfirmation": "Are you sure, you want to continue?"
}, },
"mathEquation": { "mathEquation": {
"addMathEquation": "Add Math Equation", "addMathEquation": "Add a TeX equation",
"editMathEquation": "Edit Math Equation" "editMathEquation": "Edit Math Equation"
}, },
"optionAction": { "optionAction": {
@ -676,7 +677,8 @@
"copy": "Copy", "copy": "Copy",
"cut": "Cut", "cut": "Cut",
"paste": "Paste" "paste": "Paste"
} },
"action": "Actions"
}, },
"textBlock": { "textBlock": {
"placeholder": "Type '/' for commands" "placeholder": "Type '/' for commands"
@ -715,7 +717,11 @@
}, },
"searchForAnImage": "Search for an image", "searchForAnImage": "Search for an image",
"pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", "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": { "codeBlock": {
"language": { "language": {