diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 3df56c10ac..41d6dd3572 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1376,6 +1376,7 @@ "problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingImage": "Unable to Copy Image", "problemCopyingImageLink": "Unable to Copy Image Link", + "problemDownloadingImage": "Unable to Download Image", "problemDownloadingCanvas": "Problem Downloading Canvas", "problemDownloadingCanvasDesc": "Unable to export base layer", "problemImportingMask": "Problem Importing Mask", diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts new file mode 100644 index 0000000000..5c75549eac --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -0,0 +1,43 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useImageUrlToBlob } from './useImageUrlToBlob'; + +export const useDownloadImage = () => { + const toaster = useAppToaster(); + const { t } = useTranslation(); + const imageUrlToBlob = useImageUrlToBlob(); + + const downloadImage = useCallback( + async (image_url: string, image_name: string) => { + try { + const blob = await imageUrlToBlob(image_url); + + if (!blob) { + throw new Error('Unable to create Blob'); + } + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = image_name; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + } catch (err) { + toaster({ + title: t('toast.problemDownloadingImage'), + description: String(err), + status: 'error', + duration: 2500, + isClosable: true, + }); + } + }, + [t, toaster, imageUrlToBlob] + ); + + return { downloadImage }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 4a9b9c6e8b..f4d9c7a840 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -4,6 +4,7 @@ import { useAppToaster } from 'app/components/Toaster'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; +import { useDownloadImage } from 'common/hooks/useDownloadImage'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; @@ -47,7 +48,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const toaster = useAppToaster(); const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const customStarUi = useStore($customStarUI); - + const { downloadImage } = useDownloadImage(); const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(imageDTO?.image_name); const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({}); @@ -143,6 +144,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { } }, [unstarImages, imageDTO]); + const handleDownloadImage = useCallback(() => { + downloadImage(imageDTO.image_url, imageDTO.image_name); + }, [downloadImage, imageDTO.image_name, imageDTO.image_url]); + return ( <> }> @@ -153,14 +158,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.copyImage')} )} - } - w="100%" - > + } onClickCapture={handleDownloadImage}> {t('parameters.downloadImage')}