mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat: optimize copy image to clipboard (#12366)
* feat: optimize copy image to clipboard * pr feedback * fix: urlToBlob Co-authored-by: Jason Rasmussen <jason@rasm.me> * fix: imgToBlob Co-authored-by: Jason Rasmussen <jason@rasm.me> * chore: finish rename * fix: dimensions --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
ecc85ff6c6
commit
a653d9d29f
6 changed files with 52 additions and 19 deletions
6
web/package-lock.json
generated
6
web/package-lock.json
generated
|
@ -17,7 +17,6 @@
|
|||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||
"@zoom-image/svelte": "^0.2.6",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
|
@ -3284,11 +3283,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-image-clipboard": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz",
|
||||
"integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA=="
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.37.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||
|
|
|
@ -73,7 +73,6 @@
|
|||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||
"@zoom-image/svelte": "^0.2.6",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
mdiPresentationPlay,
|
||||
mdiUpload,
|
||||
} from '@mdi/js';
|
||||
import { canCopyImagesToClipboard } from 'copy-image-clipboard';
|
||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
|
@ -101,7 +101,7 @@
|
|||
on:click={onZoomImage}
|
||||
/>
|
||||
{/if}
|
||||
{#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image}
|
||||
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
|
||||
<CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} on:click={onCopyImage} />
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { isWebCompatibleImage, canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let preloadAssets: AssetResponseDto[] | undefined = undefined;
|
||||
|
@ -81,23 +81,19 @@
|
|||
};
|
||||
|
||||
copyImage = async () => {
|
||||
if (!canCopyImagesToClipboard()) {
|
||||
if (!canCopyImageToClipboard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyImageToClipboard(assetFileUrl);
|
||||
await copyImageToClipboard($photoViewer ?? assetFileUrl);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('copied_image_to_clipboard'),
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error [photo-viewer]:', error);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Copying image to clipboard failed.',
|
||||
});
|
||||
handleError(error, $t('copy_error'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { getAssetFilename, getFilenameExtension } from './asset-utils';
|
||||
import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension } from './asset-utils';
|
||||
|
||||
describe('get file extension from filename', () => {
|
||||
it('returns the extension without including the dot', () => {
|
||||
|
@ -56,3 +56,9 @@ describe('get asset filename', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('copy image to clipboard', () => {
|
||||
it('should not allow copy image to clipboard', () => {
|
||||
expect(canCopyImageToClipboard()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -527,3 +527,41 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
|
|||
export const delay = async (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
export const canCopyImageToClipboard = (): boolean => {
|
||||
return !!(navigator.clipboard && window.ClipboardItem);
|
||||
};
|
||||
|
||||
const imgToBlob = async (imageElement: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.width = imageElement.naturalWidth;
|
||||
canvas.height = imageElement.naturalHeight;
|
||||
|
||||
if (context) {
|
||||
context.drawImage(imageElement, 0, 0);
|
||||
|
||||
return await new Promise<Blob>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
throw new Error('Canvas conversion to Blob failed');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Canvas context is null');
|
||||
};
|
||||
|
||||
const urlToBlob = async (imageSource: string) => {
|
||||
const response = await fetch(imageSource);
|
||||
return await response.blob();
|
||||
};
|
||||
|
||||
export const copyImageToClipboard = async (source: HTMLImageElement | string) => {
|
||||
const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source);
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue