From ffdf5d24a047fc99a790800e02f89737db489b12 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 10 Oct 2023 12:43:31 +0800 Subject: [PATCH] fix: openAI image expiration (#3660) * feat: save the openAI image to local storage * feat: support rendering error block * fix: enter on Toggle list moves heading down without contents --- .../document/presentation/editor_page.dart | 8 ++ .../error/error_block_component_builder.dart | 100 ++++++++++++++++++ .../image/image_placeholder.dart | 60 +++++++++-- .../image/upload_image_menu.dart | 20 ++-- .../presentation/editor_plugins/plugins.dart | 1 + .../toggle/toggle_block_shortcut_event.dart | 6 ++ frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/resources/translations/en.json | 4 + 9 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index cf388b1ca4..15f293c180 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -151,6 +151,9 @@ class _AppFlowyEditorPageState extends State { effectiveScrollController = widget.scrollController ?? ScrollController(); // keep the previous font style when typing new text. + supportSlashMenuNodeWhiteList.addAll([ + ToggleListBlockKeys.type, + ]); AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily); } @@ -353,6 +356,11 @@ class _AppFlowyEditorPageState extends State { styleCustomizer.outlineBlockPlaceholderStyleBuilder(), ), ), + errorBlockComponentBuilderKey: ErrorBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 10), + ), + ), }; final builders = { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart new file mode 100644 index 0000000000..a50895d298 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.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/material.dart'; + +class ErrorBlockComponentBuilder extends BlockComponentBuilder { + ErrorBlockComponentBuilder({ + super.configuration, + }); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return ErrorBlockComponentWidget( + key: node.key, + node: node, + configuration: configuration, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + bool validate(Node node) => true; +} + +class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { + const ErrorBlockComponentWidget({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => + _DividerBlockComponentWidgetState(); +} + +class _DividerBlockComponentWidgetState extends State + with BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + @override + Widget build(BuildContext context) { + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyButton( + onTap: () async { + showSnackBarMessage( + context, + LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), + ); + await getIt().setData( + ClipboardServiceData(plainText: jsonEncode(node.toJson())), + ); + }, + text: Container( + height: 48, + alignment: Alignment.center, + child: FlowyText( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + ), + ), + ); + + child = Padding( + padding: padding, + child: child, + ); + + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + return child; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index 37102c88fd..c555b2a6e7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -15,6 +15,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:http/http.dart'; import 'package:path/path.dart' as p; import 'package:string_validator/string_validator.dart'; @@ -47,16 +48,22 @@ class _ImagePlaceholderState extends State { clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (context) { return UploadImageMenu( - onPickFile: (path) { + onSelectedLocalImage: (path) { controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - insertLocalImage(path); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertLocalImage(path); }); }, - onSubmit: (url) { + onSelectedAIImage: (url) { controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - insertNetworkImage(url); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertAIImage(url); + }); + }, + onSelectedNetworkImage: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertNetworkImage(url); }); }, ); @@ -123,7 +130,46 @@ class _ImagePlaceholderState extends State { } catch (e) { Log.error('cannot copy image file', e); } - controller.close(); + } + + Future insertAIImage(String url) async { + if (url.isEmpty || !isURL(url)) { + // show error + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_error_invalidImage.tr(), + ); + return; + } + + final path = await getIt().getPath(); + final imagePath = p.join( + path, + 'images', + ); + try { + // create the directory if not exists + final directory = Directory(imagePath); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + final uri = Uri.parse(url); + final copyToPath = p.join( + imagePath, + '${uuid()}${p.extension(uri.path)}', + ); + + final response = await get(uri); + await File(copyToPath).writeAsBytes(response.bodyBytes); + + final transaction = editorState.transaction; + transaction.updateNode(widget.node, { + ImageBlockKeys.url: copyToPath, + }); + await editorState.apply(transaction); + } catch (e) { + Log.error('cannot save image file', e); + } } Future insertNetworkImage(String url) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 82546b85be..0b8eed1f98 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -36,12 +36,14 @@ enum UploadImageType { class UploadImageMenu extends StatefulWidget { const UploadImageMenu({ super.key, - required this.onPickFile, - required this.onSubmit, + required this.onSelectedLocalImage, + required this.onSelectedAIImage, + required this.onSelectedNetworkImage, }); - final void Function(String? path) onPickFile; - final void Function(String url) onSubmit; + final void Function(String? path) onSelectedLocalImage; + final void Function(String url) onSelectedAIImage; + final void Function(String url) onSelectedNetworkImage; @override State createState() => _UploadImageMenuState(); @@ -127,14 +129,14 @@ class _UploadImageMenuState extends State { return Padding( padding: const EdgeInsets.all(8.0), child: UploadImageFileWidget( - onPickFile: widget.onPickFile, + onPickFile: widget.onSelectedLocalImage, ), ); case UploadImageType.url: return Padding( padding: const EdgeInsets.all(8.0), child: EmbedImageUrlWidget( - onSubmit: widget.onSubmit, + onSubmit: widget.onSelectedNetworkImage, ), ); case UploadImageType.unsplash: @@ -142,7 +144,7 @@ class _UploadImageMenuState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: UnsplashImageWidget( - onSelectUnsplashImage: widget.onSubmit, + onSelectUnsplashImage: widget.onSelectedNetworkImage, ), ), ); @@ -152,7 +154,7 @@ class _UploadImageMenuState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: OpenAIImageWidget( - onSelectNetworkImage: widget.onSubmit, + onSelectNetworkImage: widget.onSelectedAIImage, ), ), ) @@ -168,7 +170,7 @@ class _UploadImageMenuState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: StabilityAIImageWidget( - onSelectImage: widget.onPickFile, + onSelectImage: widget.onSelectedLocalImage, ), ), ) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 29915410c6..ceace6a955 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -12,6 +12,7 @@ export 'copy_and_paste/custom_paste_command.dart'; export 'database/database_view_block_component.dart'; export 'database/inline_database_menu_item.dart'; export 'database/referenced_database_menu_item.dart'; +export 'error/error_block_component_builder.dart'; export 'extensions/flowy_tint_extension.dart'; export 'find_and_replace/find_and_replace_menu.dart'; export 'font/customize_font_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart index df44033f50..8e9ad2305b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart @@ -60,6 +60,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( ..afterSelection = Selection.collapsed( Position(path: selection.start.path, offset: 0), ); + } else if (selection.startIndex == 0) { + // insert a paragraph block above the current toggle list block + transaction.insertNode(selection.start.path, paragraphNode()); + transaction.afterSelection = Selection.collapsed( + Position(path: selection.start.path.next, offset: 0), + ); } else { // insert a toggle list block below the current toggle list block transaction diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 34722840f6..5b6d3c5b4a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,8 +54,8 @@ packages: dependency: "direct main" description: path: "." - ref: "0abcf7f" - resolved-ref: "0abcf7f6d273b838c895abdc17f6833540613729" + ref: adb05d4 + resolved-ref: adb05d4c49fe2f518e5554cc7d6c2fbe3b01670d url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "1.4.3" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0d608c8c67..4822b0e3ba 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "0abcf7f" + ref: "adb05d4" appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3fccb61e65..b9b808fc5a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -705,6 +705,10 @@ }, "toolbar": { "resetToDefaultFont": "Reset to default" + }, + "errorBlock": { + "theBlockIsNotSupported": "The current version does not support this block.", + "blockContentHasBeenCopied": "The block content has been copied." } }, "board": {