Przeglądaj źródła

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
Lucas.Xu 1 rok temu
rodzic
commit
ffdf5d24a0

+ 8 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -151,6 +151,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
     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<AppFlowyEditorPage> {
               styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
         ),
       ),
+      errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          padding: (_) => const EdgeInsets.symmetric(vertical: 10),
+        ),
+      ),
     };
 
     final builders = {

+ 100 - 0
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<ErrorBlockComponentWidget> createState() =>
+      _DividerBlockComponentWidgetState();
+}
+
+class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
+    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<ClipboardService>().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;
+  }
+}

+ 53 - 7
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<ImagePlaceholder> {
       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<ImagePlaceholder> {
     } catch (e) {
       Log.error('cannot copy image file', e);
     }
-    controller.close();
+  }
+
+  Future<void> insertAIImage(String url) async {
+    if (url.isEmpty || !isURL(url)) {
+      // show error
+      showSnackBarMessage(
+        context,
+        LocaleKeys.document_imageBlock_error_invalidImage.tr(),
+      );
+      return;
+    }
+
+    final path = await getIt<ApplicationDataStorage>().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<void> insertNetworkImage(String url) async {

+ 11 - 9
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<UploadImageMenu> createState() => _UploadImageMenuState();
@@ -127,14 +129,14 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
         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<UploadImageMenu> {
           child: Padding(
             padding: const EdgeInsets.all(8.0),
             child: UnsplashImageWidget(
-              onSelectUnsplashImage: widget.onSubmit,
+              onSelectUnsplashImage: widget.onSelectedNetworkImage,
             ),
           ),
         );
@@ -152,7 +154,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
                 child: Padding(
                   padding: const EdgeInsets.all(8.0),
                   child: OpenAIImageWidget(
-                    onSelectNetworkImage: widget.onSubmit,
+                    onSelectNetworkImage: widget.onSelectedAIImage,
                   ),
                 ),
               )
@@ -168,7 +170,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
                 child: Padding(
                   padding: const EdgeInsets.all(8.0),
                   child: StabilityAIImageWidget(
-                    onSelectImage: widget.onPickFile,
+                    onSelectImage: widget.onSelectedLocalImage,
                   ),
                 ),
               )

+ 1 - 0
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';

+ 6 - 0
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

+ 2 - 2
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"

+ 1 - 1
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
 

+ 4 - 0
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": {