mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 01:54:37 +00:00 
			
		
		
		
	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
This commit is contained in:
		
							parent
							
								
									b247a49417
								
							
						
					
					
						commit
						ffdf5d24a0
					
				| @ -151,6 +151,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { | |||||||
|     effectiveScrollController = widget.scrollController ?? ScrollController(); |     effectiveScrollController = widget.scrollController ?? ScrollController(); | ||||||
| 
 | 
 | ||||||
|     // keep the previous font style when typing new text. |     // keep the previous font style when typing new text. | ||||||
|  |     supportSlashMenuNodeWhiteList.addAll([ | ||||||
|  |       ToggleListBlockKeys.type, | ||||||
|  |     ]); | ||||||
|     AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily); |     AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -353,6 +356,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { | |||||||
|               styleCustomizer.outlineBlockPlaceholderStyleBuilder(), |               styleCustomizer.outlineBlockPlaceholderStyleBuilder(), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  |       errorBlockComponentBuilderKey: ErrorBlockComponentBuilder( | ||||||
|  |         configuration: configuration.copyWith( | ||||||
|  |           padding: (_) => const EdgeInsets.symmetric(vertical: 10), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     final builders = { |     final builders = { | ||||||
|  | |||||||
| @ -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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -15,6 +15,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; | |||||||
| import 'package:flowy_infra_ui/style_widget/hover.dart'; | import 'package:flowy_infra_ui/style_widget/hover.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | import 'package:flutter_bloc/flutter_bloc.dart'; | ||||||
|  | import 'package:http/http.dart'; | ||||||
| import 'package:path/path.dart' as p; | import 'package:path/path.dart' as p; | ||||||
| import 'package:string_validator/string_validator.dart'; | import 'package:string_validator/string_validator.dart'; | ||||||
| 
 | 
 | ||||||
| @ -47,16 +48,22 @@ class _ImagePlaceholderState extends State<ImagePlaceholder> { | |||||||
|       clickHandler: PopoverClickHandler.gestureDetector, |       clickHandler: PopoverClickHandler.gestureDetector, | ||||||
|       popupBuilder: (context) { |       popupBuilder: (context) { | ||||||
|         return UploadImageMenu( |         return UploadImageMenu( | ||||||
|           onPickFile: (path) { |           onSelectedLocalImage: (path) { | ||||||
|             controller.close(); |             controller.close(); | ||||||
|             WidgetsBinding.instance.addPostFrameCallback((timeStamp) { |             WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { | ||||||
|               insertLocalImage(path); |               await insertLocalImage(path); | ||||||
|             }); |             }); | ||||||
|           }, |           }, | ||||||
|           onSubmit: (url) { |           onSelectedAIImage: (url) { | ||||||
|             controller.close(); |             controller.close(); | ||||||
|             WidgetsBinding.instance.addPostFrameCallback((timeStamp) { |             WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { | ||||||
|               insertNetworkImage(url); |               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) { |     } catch (e) { | ||||||
|       Log.error('cannot copy image file', 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 { |   Future<void> insertNetworkImage(String url) async { | ||||||
|  | |||||||
| @ -36,12 +36,14 @@ enum UploadImageType { | |||||||
| class UploadImageMenu extends StatefulWidget { | class UploadImageMenu extends StatefulWidget { | ||||||
|   const UploadImageMenu({ |   const UploadImageMenu({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.onPickFile, |     required this.onSelectedLocalImage, | ||||||
|     required this.onSubmit, |     required this.onSelectedAIImage, | ||||||
|  |     required this.onSelectedNetworkImage, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   final void Function(String? path) onPickFile; |   final void Function(String? path) onSelectedLocalImage; | ||||||
|   final void Function(String url) onSubmit; |   final void Function(String url) onSelectedAIImage; | ||||||
|  |   final void Function(String url) onSelectedNetworkImage; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   State<UploadImageMenu> createState() => _UploadImageMenuState(); |   State<UploadImageMenu> createState() => _UploadImageMenuState(); | ||||||
| @ -127,14 +129,14 @@ class _UploadImageMenuState extends State<UploadImageMenu> { | |||||||
|         return Padding( |         return Padding( | ||||||
|           padding: const EdgeInsets.all(8.0), |           padding: const EdgeInsets.all(8.0), | ||||||
|           child: UploadImageFileWidget( |           child: UploadImageFileWidget( | ||||||
|             onPickFile: widget.onPickFile, |             onPickFile: widget.onSelectedLocalImage, | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       case UploadImageType.url: |       case UploadImageType.url: | ||||||
|         return Padding( |         return Padding( | ||||||
|           padding: const EdgeInsets.all(8.0), |           padding: const EdgeInsets.all(8.0), | ||||||
|           child: EmbedImageUrlWidget( |           child: EmbedImageUrlWidget( | ||||||
|             onSubmit: widget.onSubmit, |             onSubmit: widget.onSelectedNetworkImage, | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       case UploadImageType.unsplash: |       case UploadImageType.unsplash: | ||||||
| @ -142,7 +144,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> { | |||||||
|           child: Padding( |           child: Padding( | ||||||
|             padding: const EdgeInsets.all(8.0), |             padding: const EdgeInsets.all(8.0), | ||||||
|             child: UnsplashImageWidget( |             child: UnsplashImageWidget( | ||||||
|               onSelectUnsplashImage: widget.onSubmit, |               onSelectUnsplashImage: widget.onSelectedNetworkImage, | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
| @ -152,7 +154,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> { | |||||||
|                 child: Padding( |                 child: Padding( | ||||||
|                   padding: const EdgeInsets.all(8.0), |                   padding: const EdgeInsets.all(8.0), | ||||||
|                   child: OpenAIImageWidget( |                   child: OpenAIImageWidget( | ||||||
|                     onSelectNetworkImage: widget.onSubmit, |                     onSelectNetworkImage: widget.onSelectedAIImage, | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ) |               ) | ||||||
| @ -168,7 +170,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> { | |||||||
|                 child: Padding( |                 child: Padding( | ||||||
|                   padding: const EdgeInsets.all(8.0), |                   padding: const EdgeInsets.all(8.0), | ||||||
|                   child: StabilityAIImageWidget( |                   child: StabilityAIImageWidget( | ||||||
|                     onSelectImage: widget.onPickFile, |                     onSelectImage: widget.onSelectedLocalImage, | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ) |               ) | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ export 'copy_and_paste/custom_paste_command.dart'; | |||||||
| export 'database/database_view_block_component.dart'; | export 'database/database_view_block_component.dart'; | ||||||
| export 'database/inline_database_menu_item.dart'; | export 'database/inline_database_menu_item.dart'; | ||||||
| export 'database/referenced_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 'extensions/flowy_tint_extension.dart'; | ||||||
| export 'find_and_replace/find_and_replace_menu.dart'; | export 'find_and_replace/find_and_replace_menu.dart'; | ||||||
| export 'font/customize_font_toolbar_item.dart'; | export 'font/customize_font_toolbar_item.dart'; | ||||||
|  | |||||||
| @ -60,6 +60,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( | |||||||
|           ..afterSelection = Selection.collapsed( |           ..afterSelection = Selection.collapsed( | ||||||
|             Position(path: selection.start.path, offset: 0), |             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 { |       } else { | ||||||
|         // insert a toggle list block below the current toggle list block |         // insert a toggle list block below the current toggle list block | ||||||
|         transaction |         transaction | ||||||
|  | |||||||
| @ -54,8 +54,8 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       path: "." |       path: "." | ||||||
|       ref: "0abcf7f" |       ref: adb05d4 | ||||||
|       resolved-ref: "0abcf7f6d273b838c895abdc17f6833540613729" |       resolved-ref: adb05d4c49fe2f518e5554cc7d6c2fbe3b01670d | ||||||
|       url: "https://github.com/AppFlowy-IO/appflowy-editor.git" |       url: "https://github.com/AppFlowy-IO/appflowy-editor.git" | ||||||
|     source: git |     source: git | ||||||
|     version: "1.4.3" |     version: "1.4.3" | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ dependencies: | |||||||
|   appflowy_editor: |   appflowy_editor: | ||||||
|     git: |     git: | ||||||
|       url: https://github.com/AppFlowy-IO/appflowy-editor.git |       url: https://github.com/AppFlowy-IO/appflowy-editor.git | ||||||
|       ref: "0abcf7f" |       ref: "adb05d4" | ||||||
|   appflowy_popover: |   appflowy_popover: | ||||||
|     path: packages/appflowy_popover |     path: packages/appflowy_popover | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -705,6 +705,10 @@ | |||||||
|     }, |     }, | ||||||
|     "toolbar": { |     "toolbar": { | ||||||
|       "resetToDefaultFont": "Reset to default" |       "resetToDefaultFont": "Reset to default" | ||||||
|  |     }, | ||||||
|  |     "errorBlock": { | ||||||
|  |       "theBlockIsNotSupported": "The current version does not support this block.", | ||||||
|  |       "blockContentHasBeenCopied": "The block content has been copied." | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "board": { |   "board": { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Lucas.Xu
						Lucas.Xu