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:
@ -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();
|
||||
},
|
||||
);
|
Reference in New Issue
Block a user