diff --git a/web/src/lib/components/album-page/album-description.svelte b/web/src/lib/components/album-page/album-description.svelte index 9e988ba75e..e860fbcd21 100644 --- a/web/src/lib/components/album-page/album-description.svelte +++ b/web/src/lib/components/album-page/album-description.svelte @@ -2,6 +2,7 @@ import { autoGrowHeight } from '$lib/utils/autogrow'; import { updateAlbumInfo } from '@immich/sdk'; import { handleError } from '$lib/utils/handle-error'; + import { shortcut } from '$lib/utils/shortcut'; export let id: string; export let description: string; @@ -37,6 +38,10 @@ on:focusout={handleUpdateDescription} use:autoGrowHeight placeholder="Add description" + use:shortcut={{ + shortcut: { key: 'Enter', ctrl: true }, + onShortcut: (e) => e.currentTarget.blur(), + }} /> {:else if description}

diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 8a4af18eae..2725bb8921 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -1,6 +1,7 @@ e.key === 'Enter' && e.currentTarget.blur()} + use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }} on:blur={handleUpdateName} class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index ab5eb91d5e..591f90fff8 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,11 +1,9 @@ + { + if (!$showAssetViewer && $isMultiSelectState) { + assetInteractionStore.clearMultiselect(); + } + }, + }} +/> +

{#if $isMultiSelectState} { - if (event.key === 'Enter') { - event.preventDefault(); - await handleSendComment(); - return; - } - }; - const timeOptions = { year: 'numeric', month: '2-digit', @@ -295,7 +288,10 @@ use:autoGrowHeight={'5px'} placeholder={disabled ? 'Comments are disabled' : 'Say something'} on:input={() => autoGrowHeight(textArea, '5px')} - on:keypress={handleEnter} + use:shortcut={{ + shortcut: { key: 'Enter' }, + onShortcut: () => handleSendComment(), + }} class="h-[18px] {disabled ? 'cursor-not-allowed' : ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200" diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 0af40b4d45..2ddd7ada4f 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -13,7 +13,7 @@ import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils'; import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; - import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; + import { shortcuts } from '$lib/utils/shortcut'; import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { AssetJobName, @@ -256,68 +256,9 @@ isShowActivity = !isShowActivity; }; - const handleKeypress = async (event: KeyboardEvent) => { - if (shouldIgnoreShortcut(event)) { - return; - } - - const key = event.key; - const shiftKey = event.shiftKey; - const ctrlKey = event.ctrlKey; - - if (ctrlKey) { - return; - } - - switch (key) { - case 'a': - case 'A': { - if (shiftKey) { - await toggleArchive(); - } - return; - } - case 'ArrowLeft': { - await navigateAsset('previous'); - return; - } - case 'ArrowRight': { - await navigateAsset('next'); - return; - } - case 'd': - case 'D': { - if (shiftKey) { - await downloadFile(asset); - } - return; - } - case 'Delete': { - await trashOrDelete(shiftKey); - return; - } - case 'Escape': { - if (isShowDeleteConfirmation) { - isShowDeleteConfirmation = false; - return; - } - if (isShowShareModal) { - isShowShareModal = false; - return; - } - closeViewer(); - return; - } - case 'f': { - await toggleFavorite(); - return; - } - case 'i': { - isShowActivity = false; - $isShowDetail = !$isShowDetail; - return; - } - } + const toggleDetailPanel = () => { + isShowActivity = false; + $isShowDetail = !$isShowDetail; }; const handleCloseViewer = () => { @@ -557,7 +498,19 @@ }; - + navigateAsset('previous') }, + { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, + { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(false) }, + { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, + { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, + { shortcut: { key: 'f' }, onShortcut: toggleFavorite }, + { shortcut: { key: 'i' }, onShortcut: toggleDetailPanel }, + ]} +/>
(); - const handleKeypress = async (event: KeyboardEvent) => { - if (event.target !== textArea) { - return; - } - const ctrl = event.ctrlKey; - switch (event.key) { - case 'Enter': { - if (ctrl && event.target === textArea) { - await handleFocusOut(); - } - } - } - }; - const getMegapixel = (width: number, height: number): number | undefined => { const megapixel = Math.round((height * width) / 1_000_000); @@ -180,8 +167,6 @@ } - -
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 2093a60b8b..14bbb6b1fa 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -6,7 +6,7 @@ import { downloadRequest, getAssetFileUrl, handlePromiseError } from '$lib/utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; - import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; + import { shortcuts } from '$lib/utils/shortcut'; import { type AssetResponseDto, AssetTypeEnum } from '@immich/sdk'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { onDestroy, onMount } from 'svelte'; @@ -84,18 +84,6 @@ } }; - const handleKeypress = async (event: KeyboardEvent) => { - if (shouldIgnoreShortcut(event)) { - return; - } - if (window.getSelection()?.type === 'Range') { - return; - } - if ((event.metaKey || event.ctrlKey) && event.key === 'c') { - await doCopy(); - } - }; - const doCopy = async () => { if (!canCopyImagesToClipboard()) { return; @@ -138,9 +126,23 @@ handlePromiseError(loadAssetData({ loadOriginal: true })); } }); + + const onCopyShortcut = () => { + if (window.getSelection()?.type === 'Range') { + return; + } + handlePromiseError(doCopy()); + }; - +
{ - if (e.key === 'ArrowRight' && canGoForward) { - e.preventDefault(); - await toNext(); - } else if (e.key === 'ArrowLeft' && canGoBack) { - e.preventDefault(); - await toPrevious(); - } else if (e.key === 'Escape') { - e.preventDefault(); - await goto(AppRoute.PHOTOS); - } - }; - onMount(async () => { if (!$memoryStore) { const localTime = new Date(); @@ -101,7 +89,13 @@ let galleryInView = false; - + canGoForward && toNext() }, + { shortcut: { key: 'ArrowLeft' }, onShortcut: () => canGoBack && toPrevious() }, + { shortcut: { key: 'Escape' }, onShortcut: () => goto(AppRoute.PHOTOS) }, + ]} +/>
{#if currentMemory} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 772b039f80..cf2619026e 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,5 +1,4 @@ - + {#if isShowDeleteConfirmation} { - if (event.key !== 'Escape') { + if (!matchesShortcut(event, { key: 'Escape' })) { return; } diff --git a/web/src/lib/utils/shortcut.ts b/web/src/lib/utils/shortcut.ts index 51a64a6c5b..721b91d76c 100644 --- a/web/src/lib/utils/shortcut.ts +++ b/web/src/lib/utils/shortcut.ts @@ -1,7 +1,76 @@ -export const shouldIgnoreShortcut = (event: Event): boolean => { - const type = (event.target as HTMLInputElement).type; - if (['textarea', 'text'].includes(type)) { - return true; - } - return false; +import type { ActionReturn } from 'svelte/action'; + +export type Shortcut = { + key: string; + alt?: boolean; + ctrl?: boolean; + shift?: boolean; + meta?: boolean; +}; + +export type ShortcutOptions = { + shortcut: Shortcut; + onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown; +}; + +export const shouldIgnoreShortcut = (event: KeyboardEvent): boolean => { + if (event.target === event.currentTarget) { + return false; + } + const type = (event.target as HTMLInputElement).type; + return ['textarea', 'text'].includes(type); +}; + +export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { + return ( + shortcut.key.toLowerCase() === event.key.toLowerCase() && + Boolean(shortcut.alt) === event.altKey && + Boolean(shortcut.ctrl) === event.ctrlKey && + Boolean(shortcut.shift) === event.shiftKey && + Boolean(shortcut.meta) === event.metaKey + ); +}; + +export const shortcut = ( + node: T, + option: ShortcutOptions, +): ActionReturn> => { + const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]); + + return { + update(newOption) { + shortcutsUpdate?.([newOption]); + }, + destroy, + }; +}; + +export const shortcuts = ( + node: T, + options: ShortcutOptions[], +): ActionReturn[]> => { + function onKeydown(event: KeyboardEvent) { + if (shouldIgnoreShortcut(event)) { + return; + } + + for (const { shortcut, onShortcut } of options) { + if (matchesShortcut(event, shortcut)) { + event.preventDefault(); + onShortcut(event as KeyboardEvent & { currentTarget: T }); + return; + } + } + } + + node.addEventListener('keydown', onKeydown); + + return { + update(newOptions) { + options = newOptions; + }, + destroy() { + node.removeEventListener('keydown', onKeydown); + }, + }; }; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 9e272ce3f6..0d171d5a45 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -113,7 +113,6 @@ let reactions: ActivityResponseDto[] = []; let globalWidth: number; let assetGridWidth: number; - let textArea: HTMLTextAreaElement; let albumOrder: AssetOrder | undefined = data.album.order; $: assetStore = new AssetStore({ albumId, order: albumOrder }); @@ -225,20 +224,6 @@ handlePromiseError(getNumberOfComments()); } - const handleKeypress = (event: KeyboardEvent) => { - if (event.target !== textArea) { - return; - } - const ctrl = event.ctrlKey; - switch (event.key) { - case 'Enter': { - if (ctrl && event.target === textArea) { - textArea.blur(); - } - } - } - }; - const handleStartSlideshow = async () => { const asset = $slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0]; @@ -394,8 +379,6 @@ }; - -
{#if $isMultiSelectState} diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 22dc98ad3e..9665b8aeea 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -1,5 +1,4 @@ - + {#if showMergeModal} handleKeyboardPress(event); - - const handleKeyboardPress = async (event: KeyboardEvent) => { - if (shouldIgnoreShortcut(event)) { + const onEscape = () => { + if ($showAssetViewer) { return; } - if (!$showAssetViewer) { - switch (event.key) { - case 'Escape': { - if (isMultiSelectionMode) { - selectedAssets = new Set(); - return; - } - if (!$preventRaceConditionSearchBar) { - await goto(previousRoute); - } - $preventRaceConditionSearchBar = false; - return; - } - } + + if (isMultiSelectionMode) { + selectedAssets = new Set(); + return; } + if (!$preventRaceConditionSearchBar) { + handlePromiseError(goto(previousRoute)); + } + $preventRaceConditionSearchBar = false; }; afterNavigate(({ from }) => { @@ -201,7 +193,7 @@ } - +
{#if isMultiSelectionMode}