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

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

View File

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

View File

@ -245,7 +245,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
contextMenuItems: customContextMenuItems,
// customize the header and footer.
header: widget.header,
footer: const VSpace(200),
footer: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400),
),
);
@ -285,7 +285,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
linkMobileToolbarItem,
quoteMobileToolbarItem,
dividerMobileToolbarItem,
imageMobileToolbarItem,
mathEquationMobileToolbarItem,
codeMobileToolbarItem,
undoMobileToolbarItem,
redoMobileToolbarItem,
],
),
],

View File

@ -0,0 +1,107 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// The ... button shows on the top right corner of a block.
///
/// Default actions are:
/// - delete
/// - duplicate
/// - insert above
/// - insert below
///
/// Only works on mobile.
class MobileBlockActionButtons extends StatelessWidget {
const MobileBlockActionButtons({
super.key,
this.extendActionWidgets = const [],
required this.node,
required this.editorState,
required this.child,
});
final Node node;
final EditorState editorState;
final List<Widget> extendActionWidgets;
final Widget child;
@override
Widget build(BuildContext context) {
if (!PlatformExtension.isMobile) {
return child;
}
const padding = 5.0;
return Stack(
children: [
child,
Positioned(
top: padding,
right: padding,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.three_dots_s,
),
width: 20.0,
onPressed: () => _showBottomSheet(context),
),
),
],
);
}
void _showBottomSheet(BuildContext context) {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(
extendActionWidgets: extendActionWidgets,
onAction: (action) async {
context.pop();
final transaction = editorState.transaction;
switch (action) {
case BlockActionBottomSheetType.delete:
transaction.deleteNode(node);
break;
case BlockActionBottomSheetType.duplicate:
transaction.insertNode(
node.path.next,
node.copyWith(),
);
break;
case BlockActionBottomSheetType.insertAbove:
case BlockActionBottomSheetType.insertBelow:
final path = action == BlockActionBottomSheetType.insertAbove
? node.path
: node.path.next;
transaction
..insertNode(
path,
paragraphNode(),
)
..afterSelection = Selection.collapsed(
Position(
path: path,
),
);
break;
default:
}
if (transaction.operations.isNotEmpty) {
await editorState.apply(transaction);
}
},
);
},
);
}
}

View File

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

View File

@ -1,7 +1,24 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
const kImagePlaceholderKey = 'imagePlaceholderKey';
@ -96,25 +113,30 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
final height = attributes[ImageBlockKeys.height]?.toDouble();
final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey];
Widget child = src.isEmpty
? ImagePlaceholder(
key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,
node: node,
)
: ResizableImage(
src: src,
width: width,
height: height,
editable: editorState.editable,
alignment: alignment,
onResize: (width) {
final transaction = editorState.transaction
..updateNode(node, {
ImageBlockKeys.width: width,
});
editorState.apply(transaction);
},
);
Widget child;
if (src.isEmpty) {
child = ImagePlaceholder(
key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,
node: node,
);
} else if (!_checkIfURLIsValid(src)) {
child = const UnSupportImageWidget();
} else {
child = ResizableImage(
src: src,
width: width,
height: height,
editable: editorState.editable,
alignment: alignment,
onResize: (width) {
final transaction = editorState.transaction
..updateNode(node, {
ImageBlockKeys.width: width,
});
editorState.apply(transaction);
},
);
}
child = BlockSelectionContainer(
node: node,
@ -139,40 +161,51 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
);
}
if (widget.showMenu && widget.menuBuilder != null) {
child = MouseRegion(
onEnter: (_) => showActionsNotifier.value = true,
onExit: (_) {
if (!alwaysShowMenu) {
showActionsNotifier.value = false;
}
},
hitTestBehavior: HitTestBehavior.opaque,
opaque: false,
child: ValueListenableBuilder<bool>(
valueListenable: showActionsNotifier,
builder: (context, value, child) {
final url = node.attributes[ImageBlockKeys.url];
return Stack(
children: [
BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
child: child!,
),
if (value && url.isNotEmpty == true)
widget.menuBuilder!(
widget.node,
this,
),
],
);
// show a hover menu on desktop or web
if (PlatformExtension.isDesktopOrWeb) {
if (widget.showMenu && widget.menuBuilder != null) {
child = MouseRegion(
onEnter: (_) => showActionsNotifier.value = true,
onExit: (_) {
if (!alwaysShowMenu) {
showActionsNotifier.value = false;
}
},
child: child,
),
hitTestBehavior: HitTestBehavior.opaque,
opaque: false,
child: ValueListenableBuilder<bool>(
valueListenable: showActionsNotifier,
builder: (context, value, child) {
final url = node.attributes[ImageBlockKeys.url];
return Stack(
children: [
BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
child: child!,
),
if (value && url.isNotEmpty == true)
widget.menuBuilder!(
widget.node,
this,
),
],
);
},
child: child,
),
);
}
} else {
// show a fixed menu on mobile
child = MobileBlockActionButtons(
node: node,
editorState: editorState,
extendActionWidgets: _buildExtendActionWidgets(context),
child: child,
);
}
@ -246,4 +279,89 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
bool shiftWithBaseOffset = false,
}) =>
_renderBox!.localToGlobal(offset);
// only used on mobile platform
List<Widget> _buildExtendActionWidgets(BuildContext context) {
final url = widget.node.attributes[ImageBlockKeys.url];
if (!_checkIfURLIsValid(url)) {
return [];
}
return [
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.copy_s,
text: LocaleKeys.editor_copyLink.tr(),
onTap: () async {
context.pop();
showSnackBarMessage(
context,
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
);
await getIt<ClipboardService>().setPlainText(url);
},
),
),
const HSpace(8.0),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.image_placeholder_s,
text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(),
onTap: () async {
context.pop();
Uint8List? bytes;
if (isURL(url)) {
// network image
final result = await get(Uri.parse(url));
if (result.statusCode == 200) {
bytes = result.bodyBytes;
}
} else {
final file = File(url);
bytes = await file.readAsBytes();
}
if (bytes != null) {
await ImageGallerySaver.saveImage(bytes);
if (context.mounted) {
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_successToAddImageToGallery
.tr(),
);
}
} else {
if (context.mounted) {
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_failedToAddImageToGallery
.tr(),
);
}
}
},
),
),
],
),
const VSpace(8),
];
}
bool _checkIfURLIsValid(dynamic url) {
if (url is! String) {
return false;
}
if (url.isEmpty) {
return false;
}
if (!isURL(url) && !File(url).existsSync()) {
return false;
}
return true;
}
}

View File

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

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
@ -15,6 +16,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart';
import 'package:path/path.dart' as p;
import 'package:string_validator/string_validator.dart';
@ -37,65 +39,114 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: controller,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: const BoxConstraints(
maxWidth: 540,
maxHeight: 360,
minHeight: 80,
final Widget child = DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (context) {
return UploadImageMenu(
onSelectedLocalImage: (path) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertLocalImage(path);
});
},
onSelectedAIImage: (url) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertAIImage(url);
});
},
onSelectedNetworkImage: (url) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertNetworkImage(url);
});
},
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
),
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
),
child: SizedBox(
height: 48,
child: Row(
children: [
const HSpace(10),
const FlowySvg(
FlowySvgs.image_placeholder_s,
size: Size.square(24),
),
const HSpace(10),
FlowyText(
LocaleKeys.document_plugins_image_addAnImage.tr(),
),
],
),
child: SizedBox(
height: 52,
child: Row(
children: [
const HSpace(10),
const FlowySvg(
FlowySvgs.image_placeholder_s,
size: Size.square(24),
),
const HSpace(10),
FlowyText(
LocaleKeys.document_plugins_image_addAnImage.tr(),
),
],
),
),
),
);
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover(
controller: controller,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: const BoxConstraints(
maxWidth: 540,
maxHeight: 360,
minHeight: 80,
),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (context) {
return UploadImageMenu(
onSelectedLocalImage: (path) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertLocalImage(path);
});
},
onSelectedAIImage: (url) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertAIImage(url);
});
},
onSelectedNetworkImage: (url) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await insertNetworkImage(url);
});
},
);
},
child: child,
);
} else {
return GestureDetector(
onTap: () {
showUploadImageMenu();
},
child: child,
);
}
}
void showUploadImageMenu() {
if (PlatformExtension.isDesktopOrWeb) {
controller.show();
} else {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.editor_image.tr(),
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: UploadImageMenu(
supportTypes: const [
UploadImageType.local,
UploadImageType.url,
UploadImageType.unsplash,
],
onSelectedLocalImage: (path) async {
context.pop();
await insertLocalImage(path);
},
onSelectedAIImage: (url) async {
context.pop();
await insertAIImage(url);
},
onSelectedNetworkImage: (url) async {
context.pop();
await insertNetworkImage(url);
},
),
);
},
);
}
}
Future<void> insertLocalImage(String? url) async {

View File

@ -0,0 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final imageMobileToolbarItem = MobileToolbarItem.action(
itemIcon: const FlowySvg(FlowySvgs.m_toolbar_imae_lg),
actionHandler: (editorState, selection) async {
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
imagePlaceholderKey.currentState?.showUploadImageMenu();
});
},
);

View File

@ -0,0 +1,43 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
class UnSupportImageWidget extends StatelessWidget {
const UnSupportImageWidget({
super.key,
});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
),
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
),
child: SizedBox(
height: 52,
child: Row(
children: [
const HSpace(10),
const FlowySvg(
FlowySvgs.image_placeholder_s,
size: Size.square(24),
),
const HSpace(10),
FlowyText(
LocaleKeys.document_imageBlock_unableToLoadImage.tr(),
),
],
),
),
),
);
}
}

View File

@ -1,10 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class UploadImageFileWidget extends StatelessWidget {
const UploadImageFileWidget({
@ -19,31 +21,34 @@ class UploadImageFileWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyHover(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (_) async {
final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: '',
allowMultiple: false,
type: FileType.image,
allowedExtensions: allowedExtensions,
);
onPickFile(result?.files.firstOrNull?.path);
},
child: Container(
child: FlowyButton(
showDefaultBoxDecorationOnMobile: true,
text: Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.surfaceVariant,
width: 1.0,
),
),
child: FlowyText(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
),
),
onTap: _uploadImage,
),
);
}
Future<void> _uploadImage() async {
if (PlatformExtension.isDesktopOrWeb) {
// on desktop, the users can pick a image file from folder
final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: '',
allowMultiple: false,
type: FileType.image,
allowedExtensions: allowedExtensions,
);
onPickFile(result?.files.firstOrNull?.path);
} else {
// on mobile, the users can pick a image file from camera or image library
final result = await ImagePicker().pickImage(source: ImageSource.gallery);
onPickFile(result?.path);
}
}
}

View File

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

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flutter/material.dart';
@ -44,7 +46,7 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
final mathEquationState =
editorState.getNodeAtPath(path)?.key.currentState;
if (mathEquationState != null &&
mathEquationState is _MathEquationBlockComponentWidgetState) {
mathEquationState is MathEquationBlockComponentWidgetState) {
mathEquationState.showEditingDialog();
}
});
@ -89,10 +91,10 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget {
@override
State<MathEquationBlockComponentWidget> createState() =>
_MathEquationBlockComponentWidgetState();
MathEquationBlockComponentWidgetState();
}
class _MathEquationBlockComponentWidgetState
class MathEquationBlockComponentWidgetState
extends State<MathEquationBlockComponentWidget>
with BlockComponentConfigurable {
@override
@ -112,35 +114,34 @@ class _MathEquationBlockComponentWidgetState
return InkWell(
onHover: (value) => setState(() => isHover = value),
onTap: showEditingDialog,
child: _buildMathEquation(context),
child: _build(context),
);
}
Widget _buildMathEquation(BuildContext context) {
Widget _build(BuildContext context) {
Widget child = Container(
width: double.infinity,
constraints: const BoxConstraints(minHeight: 50),
padding: padding,
constraints: const BoxConstraints(minHeight: 52),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: isHover || formula.isEmpty
? Theme.of(context).colorScheme.tertiaryContainer
: Colors.transparent,
color: formula.isNotEmpty
? Colors.transparent
: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
),
child: formula.isEmpty
? FlowyText.medium(
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
fontSize: 16,
)
: Math.tex(
formula,
textStyle: const TextStyle(fontSize: 20),
mathStyle: MathStyle.display,
),
? _buildPlaceholderWidget(context)
: _buildMathEquation(context),
),
);
child = Padding(
padding: padding,
child: child,
);
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
node: node,
@ -149,9 +150,43 @@ class _MathEquationBlockComponentWidgetState
);
}
if (PlatformExtension.isMobile) {
child = MobileBlockActionButtons(
node: node,
editorState: editorState,
child: child,
);
}
return child;
}
Widget _buildPlaceholderWidget(BuildContext context) {
return SizedBox(
height: 52,
child: Row(
children: [
const HSpace(10),
const Icon(Icons.text_fields_outlined),
const HSpace(10),
FlowyText(
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
),
],
),
);
}
Widget _buildMathEquation(BuildContext context) {
return Center(
child: Math.tex(
formula,
textStyle: const TextStyle(fontSize: 20),
mathStyle: MathStyle.display,
),
);
}
void showEditingDialog() {
showDialog(
context: context,

View File

@ -0,0 +1,43 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final mathEquationMobileToolbarItem = MobileToolbarItem.action(
itemIcon: const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)),
actionHandler: (editorState, selection) async {
if (!selection.isCollapsed) {
return;
}
final path = selection.start.path;
final node = editorState.getNodeAtPath(path);
final delta = node?.delta;
if (node == null || delta == null) {
return;
}
final transaction = editorState.transaction;
final insertedNode = mathEquationNode();
if (delta.isEmpty) {
transaction
..insertNode(path, insertedNode)
..deleteNode(node);
} else {
transaction.insertNode(
path.next,
insertedNode,
);
}
await editorState.apply(transaction);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final mathEquationState =
editorState.getNodeAtPath(path)?.key.currentState;
if (mathEquationState != null &&
mathEquationState is MathEquationBlockComponentWidgetState) {
mathEquationState.showEditingDialog();
}
});
},
);

View File

@ -21,9 +21,11 @@ export 'header/custom_cover_picker.dart';
export 'header/document_header_node_widget.dart';
export 'image/image_menu.dart';
export 'image/image_selection_menu.dart';
export 'image/mobile_image_toolbar_item.dart';
export 'inline_math_equation/inline_math_equation.dart';
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
export 'math_equation/math_equation_block_component.dart';
export 'math_equation/mobile_math_eqaution_toolbar_item.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
@ -33,3 +35,5 @@ export 'table/table_menu.dart';
export 'table/table_option_action.dart';
export 'toggle/toggle_block_component.dart';
export 'toggle/toggle_block_shortcut_event.dart';
export 'undo_redo/redo_mobile_toolbar_item.dart';
export 'undo_redo/undo_mobile_toolbar_item.dart';

View File

@ -0,0 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final redoMobileToolbarItem = MobileToolbarItem.action(
itemIcon: const FlowySvg(FlowySvgs.m_redo_m),
actionHandler: (editorState, selection) async {
editorState.undoManager.redo();
},
);

View File

@ -0,0 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final undoMobileToolbarItem = MobileToolbarItem.action(
itemIcon: const FlowySvg(FlowySvgs.m_undo_m),
actionHandler: (editorState, selection) async {
editorState.undoManager.undo();
},
);