From 26f8bbf7c6ca9e37d3d9f4ede166055c71d8619f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 19 Feb 2024 16:24:47 +0700 Subject: [PATCH] feat: optimize image upload process and display an error message if upload fails (#4679) * chore: optimize image upload * feat: show upload image status * chore: upload the ai image to cloud server --- .../document/application/doc_service.dart | 2 + .../header/document_header_node_widget.dart | 2 +- .../image/image_placeholder.dart | 39 +++++++++++++------ .../editor_plugins/image/image_util.dart | 9 +++-- .../image/resizeable_image.dart | 1 + .../lib/shared/cloud_image_checker.dart | 20 ++++++++++ frontend/resources/translations/en.json | 3 +- 7 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart index 74684a19d5..9009ea52ff 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart @@ -104,12 +104,14 @@ class DocumentService { /// Upload a file to the cloud storage. Future<Either<FlowyError, UploadedFilePB>> uploadFile({ required String localFilePath, + bool isAsync = true, }) async { final workspace = await FolderEventReadCurrentWorkspace().send(); return workspace.fold((l) async { final payload = UploadFileParamsPB( workspaceId: l.id, localFilePath: localFilePath, + isAsync: isAsync, ); final result = await DocumentEventUploadFile(payload).send(); return result.swap(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 49a85406db..784a7f246c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -615,7 +615,7 @@ class DocumentCoverState extends State<DocumentCover> { details = await saveImageToLocalStorage(details); } else { // else we should save the image to cloud storage - details = await saveImageToCloudStorage(details); + (details, _) = await saveImageToCloudStorage(details); } } widget.onChangeCover(type, details); 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 3a680d1c6b..064580b752 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 @@ -44,6 +44,8 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { final documentService = DocumentService(); late final editorState = context.read<EditorState>(); + bool showLoading = false; + @override Widget build(BuildContext context) { final Widget child = DecoratedBox( @@ -65,9 +67,19 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { size: Size.square(24), ), const HSpace(10), - FlowyText( - LocaleKeys.document_plugins_image_addAnImage.tr(), - ), + ...showLoading + ? [ + FlowyText( + LocaleKeys.document_imageBlock_imageIsUploading.tr(), + ), + const HSpace(8), + const CircularProgressIndicator.adaptive(), + ] + : [ + FlowyText( + LocaleKeys.document_plugins_image_addAnImage.tr(), + ), + ], ], ), ), @@ -188,6 +200,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { final transaction = editorState.transaction; String? path; + String? errorMessage; CustomImageType imageType = CustomImageType.local; // if the user is using local authenticator, we need to save the image to local storage @@ -195,14 +208,22 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { path = await saveImageToLocalStorage(url); } else { // else we should save the image to cloud storage - path = await saveImageToCloudStorage(url); + setState(() { + showLoading = true; + }); + (path, errorMessage) = await saveImageToCloudStorage(url); + setState(() { + showLoading = false; + }); imageType = CustomImageType.internal; } if (mounted && path == null) { showSnackBarMessage( context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), + errorMessage == null + ? LocaleKeys.document_imageBlock_error_invalidImage.tr() + : ': $errorMessage', ); return; } @@ -244,12 +265,8 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { 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); + await insertLocalImage(copyToPath); + await File(copyToPath).delete(); } catch (e) { Log.error('cannot save image file', e); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart index 12fbf93b07..d6a31bb7ea 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart @@ -34,19 +34,22 @@ Future<String?> saveImageToLocalStorage(String localImagePath) async { } } -Future<String?> saveImageToCloudStorage(String localImagePath) async { +Future<(String? path, String? errorMessage)> saveImageToCloudStorage( + String localImagePath, +) async { final documentService = DocumentService(); final result = await documentService.uploadFile( localFilePath: localImagePath, + isAsync: false, ); return result.fold( - (l) => null, + (l) => (null, l.msg), (r) async { await CustomImageCacheManager().putFile( r.url, File(localImagePath).readAsBytesSync(), ); - return r.url; + return (r.url, null); }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 7b04ca3119..5038e781a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -102,6 +102,7 @@ class _ResizableImageState extends State<ResizableImage> { progressIndicatorBuilder: (context, url, progress) => _buildLoading(context), ); + child = _cacheImage!; } else { // load local file diff --git a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart new file mode 100644 index 0000000000..5a7bac2c75 --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:http/http.dart' as http; + +Future<bool> isImageExistOnCloud({ + required String url, + required UserProfilePB userProfilePB, +}) async { + final header = <String, String>{}; + final token = userProfilePB.token; + try { + final decodedToken = jsonDecode(token); + header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; + final response = await http.get(Uri.http(url), headers: header); + return response.statusCode == 200; + } catch (_) { + return false; + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 70fb0fffc1..4b5c29da69 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -862,7 +862,8 @@ "successToAddImageToGallery": "Image added to gallery successfully", "unableToLoadImage": "Unable to load image", "maximumImageSize": "Maximum supported upload image size is 10MB", - "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB" + "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB", + "imageIsUploading": "Image is uploading" }, "codeBlock": { "language": {