mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
3e6529aeb8
commit
8116ea1dba
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
});
|
||||
},
|
||||
);
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
@ -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';
|
||||
|
@ -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();
|
||||
},
|
||||
);
|
@ -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();
|
||||
},
|
||||
);
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
3
frontend/resources/flowy_icons/32x/m_toolbar_imae.svg
Normal file
3
frontend/resources/flowy_icons/32x/m_toolbar_imae.svg
Normal 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 |
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user