1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +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:
martin 2024-09-06 15:16:59 +02:00 committed by GitHub
parent ecc85ff6c6
commit a653d9d29f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 52 additions and 19 deletions

6
web/package-lock.json generated
View file

@ -17,7 +17,6 @@
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.6",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"intl-messageformat": "^10.5.14", "intl-messageformat": "^10.5.14",
@ -3284,11 +3283,6 @@
"node": ">= 0.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": { "node_modules/core-js-compat": {
"version": "3.37.1", "version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",

View file

@ -73,7 +73,6 @@
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.6",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"intl-messageformat": "^10.5.14", "intl-messageformat": "^10.5.14",

View file

@ -41,7 +41,7 @@
mdiPresentationPlay, mdiPresentationPlay,
mdiUpload, mdiUpload,
} from '@mdi/js'; } from '@mdi/js';
import { canCopyImagesToClipboard } from 'copy-image-clipboard'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
@ -101,7 +101,7 @@
on:click={onZoomImage} on:click={onZoomImage}
/> />
{/if} {/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} /> <CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} on:click={onCopyImage} />
{/if} {/if}

View file

@ -8,17 +8,17 @@
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; 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 { getBoundingBox } from '$lib/utils/people-utils';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { type SwipeCustomEvent, swipe } from 'svelte-gestures'; import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let preloadAssets: AssetResponseDto[] | undefined = undefined; export let preloadAssets: AssetResponseDto[] | undefined = undefined;
@ -81,23 +81,19 @@
}; };
copyImage = async () => { copyImage = async () => {
if (!canCopyImagesToClipboard()) { if (!canCopyImageToClipboard()) {
return; return;
} }
try { try {
await copyImageToClipboard(assetFileUrl); await copyImageToClipboard($photoViewer ?? assetFileUrl);
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: $t('copied_image_to_clipboard'), message: $t('copied_image_to_clipboard'),
timeout: 3000, timeout: 3000,
}); });
} catch (error) { } catch (error) {
console.error('Error [photo-viewer]:', error); handleError(error, $t('copy_error'));
notificationController.show({
type: NotificationType.Error,
message: 'Copying image to clipboard failed.',
});
} }
}; };

View file

@ -1,5 +1,5 @@
import type { AssetResponseDto } from '@immich/sdk'; 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', () => { describe('get file extension from filename', () => {
it('returns the extension without including the dot', () => { 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);
});
});

View file

@ -527,3 +527,41 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
export const delay = async (ms: number) => { export const delay = async (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms)); 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 })]);
};