diff --git a/i18n/en.json b/i18n/en.json index b817ab375f..c7145df889 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -920,6 +920,7 @@ "cant_get_number_of_comments": "Can't get number of comments", "cant_search_people": "Can't search people", "cant_search_places": "Can't search places", + "clipboard_unsupported_mime_type": "The system clipboard does not support copying this type of content: {mimeType}", "error_adding_assets_to_album": "Error adding assets to album", "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index c1bc039c38..a6a0f1d317 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -97,12 +97,15 @@ } try { - await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl); - notificationController.show({ - type: NotificationType.Info, - message: $t('copied_image_to_clipboard'), - timeout: 3000, - }); + const result = await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl); + if (result.success) { + notificationController.show({ type: NotificationType.Info, message: $t('copied_image_to_clipboard') }); + } else { + notificationController.show({ + type: NotificationType.Error, + message: $t('errors.clipboard_unsupported_mime_type', { values: { mimeType: result.mimeType } }), + }); + } } catch (error) { handleError(error, $t('copy_error')); } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 1e6295242a..016ca410a0 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -625,7 +625,21 @@ const urlToBlob = async (imageSource: string) => { return await response.blob(); }; -export const copyImageToClipboard = async (source: HTMLImageElement | string) => { - const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source); +export const copyImageToClipboard = async ( + source: HTMLImageElement | string, +): Promise<{ success: true } | { success: false; mimeType: string }> => { + if (source instanceof HTMLImageElement) { + // do not await, so the Safari clipboard write happens in the context of the user gesture + await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]); + return { success: true }; + } + + // if we had a way to get the mime type synchronously, we could do the same thing here + const blob = await urlToBlob(source); + if (!ClipboardItem.supports(blob.type)) { + return { success: false, mimeType: blob.type }; + } + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); + return { success: true }; };