1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 02:46:47 +01:00

fix(web): keyboard shortcut handling (#7946)

* fix(web): keyboard shortcut handling

* drop executeShortcuts in favor of action

* fix merge

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Michel Heusschen 2024-03-15 00:16:55 +01:00 committed by GitHub
parent 12fb90c232
commit eed8e6b67a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 197 additions and 268 deletions

View file

@ -2,6 +2,7 @@
import { autoGrowHeight } from '$lib/utils/autogrow'; import { autoGrowHeight } from '$lib/utils/autogrow';
import { updateAlbumInfo } from '@immich/sdk'; import { updateAlbumInfo } from '@immich/sdk';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { shortcut } from '$lib/utils/shortcut';
export let id: string; export let id: string;
export let description: string; export let description: string;
@ -37,6 +38,10 @@
on:focusout={handleUpdateDescription} on:focusout={handleUpdateDescription}
use:autoGrowHeight use:autoGrowHeight
placeholder="Add description" placeholder="Add description"
use:shortcut={{
shortcut: { key: 'Enter', ctrl: true },
onShortcut: (e) => e.currentTarget.blur(),
}}
/> />
{:else if description} {:else if description}
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base"> <p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { updateAlbumInfo } from '@immich/sdk'; import { updateAlbumInfo } from '@immich/sdk';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { shortcut } from '$lib/utils/shortcut';
export let id: string; export let id: string;
export let albumName: string; export let albumName: string;
@ -29,7 +30,7 @@
</script> </script>
<input <input
on:keydown={(e) => e.key === 'Enter' && e.currentTarget.blur()} use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
on:blur={handleUpdateName} 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 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' ? 'hover:border-gray-400'

View file

@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
import { onDestroy, onMount } from 'svelte';
import { createAssetInteractionStore } from '../../stores/asset-interaction.store'; import { createAssetInteractionStore } from '../../stores/asset-interaction.store';
import { AssetStore } from '../../stores/assets.store'; import { AssetStore } from '../../stores/assets.store';
import { downloadArchive } from '../../utils/asset-utils'; import { downloadArchive } from '../../utils/asset-utils';
@ -16,7 +14,7 @@
import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte'; import ImmichLogo from '../shared-components/immich-logo.svelte';
import ThemeButton from '../shared-components/theme-button.svelte'; import ThemeButton from '../shared-components/theme-button.svelte';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shortcut } from '$lib/utils/shortcut';
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js'; import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import AlbumSummary from './album-summary.svelte'; import AlbumSummary from './album-summary.svelte';
@ -39,39 +37,22 @@
} }
}); });
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(() => {
document.addEventListener('keydown', onKeyboardPress);
});
onDestroy(() => {
if (browser) {
document.removeEventListener('keydown', onKeyboardPress);
}
});
const handleKeyboardPress = (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) {
return;
}
if (!$showAssetViewer) {
switch (event.key) {
case 'Escape': {
if ($isMultiSelectState) {
assetInteractionStore.clearMultiselect();
}
return;
}
}
}
};
const downloadAlbum = async () => { const downloadAlbum = async () => {
await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }); await downloadArchive(`${album.albumName}.zip`, { albumId: album.id });
}; };
</script> </script>
<svelte:window
use:shortcut={{
shortcut: { key: 'Escape' },
onShortcut: () => {
if (!$showAssetViewer && $isMultiSelectState) {
assetInteractionStore.clearMultiselect();
}
},
}}
/>
<header> <header>
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar <AssetSelectControlBar

View file

@ -25,6 +25,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { shortcut } from '$lib/utils/shortcut';
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@ -95,14 +96,6 @@
} }
}; };
const handleEnter = async (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
await handleSendComment();
return;
}
};
const timeOptions = { const timeOptions = {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@ -295,7 +288,10 @@
use:autoGrowHeight={'5px'} use:autoGrowHeight={'5px'}
placeholder={disabled ? 'Comments are disabled' : 'Say something'} placeholder={disabled ? 'Comments are disabled' : 'Say something'}
on:input={() => autoGrowHeight(textArea, '5px')} on:input={() => autoGrowHeight(textArea, '5px')}
on:keypress={handleEnter} use:shortcut={{
shortcut: { key: 'Enter' },
onShortcut: () => handleSendComment(),
}}
class="h-[18px] {disabled class="h-[18px] {disabled
? 'cursor-not-allowed' ? 'cursor-not-allowed'
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200" : ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"

View file

@ -13,7 +13,7 @@
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils'; import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; 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 { SlideshowHistory } from '$lib/utils/slideshow-history';
import { import {
AssetJobName, AssetJobName,
@ -256,68 +256,9 @@
isShowActivity = !isShowActivity; isShowActivity = !isShowActivity;
}; };
const handleKeypress = async (event: KeyboardEvent) => { const toggleDetailPanel = () => {
if (shouldIgnoreShortcut(event)) { isShowActivity = false;
return; $isShowDetail = !$isShowDetail;
}
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 handleCloseViewer = () => { const handleCloseViewer = () => {
@ -557,7 +498,19 @@
}; };
</script> </script>
<svelte:window on:keydown={handleKeypress} /> <svelte:window
use:shortcuts={[
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => 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 },
]}
/>
<section <section
id="immich-asset-viewer" id="immich-asset-viewer"

View file

@ -41,6 +41,7 @@
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
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 { shortcut } from '$lib/utils/shortcut';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
@ -105,20 +106,6 @@
closeViewer: void; closeViewer: void;
}>(); }>();
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 getMegapixel = (width: number, height: number): number | undefined => {
const megapixel = Math.round((height * width) / 1_000_000); const megapixel = Math.round((height * width) / 1_000_000);
@ -180,8 +167,6 @@
} }
</script> </script>
<svelte:window on:keydown={handleKeypress} />
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> <section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2"> <div class="flex place-items-center gap-2">
<button <button
@ -223,6 +208,10 @@
use:autoGrowHeight use:autoGrowHeight
use:clickOutside use:clickOutside
on:outclick={handleFocusOut} on:outclick={handleFocusOut}
use:shortcut={{
shortcut: { key: 'Enter', ctrl: true },
onShortcut: () => handlePromiseError(handleFocusOut()),
}}
/> />
{/key} {/key}
</section> </section>

View file

@ -6,7 +6,7 @@
import { downloadRequest, getAssetFileUrl, handlePromiseError } from '$lib/utils'; import { downloadRequest, getAssetFileUrl, handlePromiseError } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-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 { type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { useZoomImageWheel } from '@zoom-image/svelte'; import { useZoomImageWheel } from '@zoom-image/svelte';
import { onDestroy, onMount } from '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 () => { const doCopy = async () => {
if (!canCopyImagesToClipboard()) { if (!canCopyImagesToClipboard()) {
return; return;
@ -138,9 +126,23 @@
handlePromiseError(loadAssetData({ loadOriginal: true })); handlePromiseError(loadAssetData({ loadOriginal: true }));
} }
}); });
const onCopyShortcut = () => {
if (window.getSelection()?.type === 'Range') {
return;
}
handlePromiseError(doCopy());
};
</script> </script>
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} /> <svelte:window
on:copyImage={doCopy}
on:zoomImage={doZoomImage}
use:shortcuts={[
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut },
{ shortcut: { key: 'c', meta: true }, onShortcut: onCopyShortcut },
]}
/>
<div <div
bind:this={element} bind:this={element}

View file

@ -9,6 +9,7 @@
import type { Viewport } from '$lib/stores/assets.store'; import type { Viewport } from '$lib/stores/assets.store';
import { memoryStore } from '$lib/stores/memory.store'; import { memoryStore } from '$lib/stores/memory.store';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { shortcuts } from '$lib/utils/shortcut';
import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js'; import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js';
@ -73,19 +74,6 @@
// Progress should be reset when the current memory or asset changes. // Progress should be reset when the current memory or asset changes.
$: memoryIndex, assetIndex, handlePromiseError(reset()); $: memoryIndex, assetIndex, handlePromiseError(reset());
const handleKeyDown = async (e: KeyboardEvent) => {
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 () => { onMount(async () => {
if (!$memoryStore) { if (!$memoryStore) {
const localTime = new Date(); const localTime = new Date();
@ -101,7 +89,13 @@
let galleryInView = false; let galleryInView = false;
</script> </script>
<svelte:window on:keydown={handleKeyDown} /> <svelte:window
use:shortcuts={[
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => canGoForward && toNext() },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => canGoBack && toPrevious() },
{ shortcut: { key: 'Escape' }, onShortcut: () => goto(AppRoute.PHOTOS) },
]}
/>
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}> <section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
{#if currentMemory} {#if currentMemory}

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
@ -9,7 +8,7 @@
import { isSearchEnabled } from '$lib/stores/search.store'; import { isSearchEnabled } from '$lib/stores/search.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { deleteAssets } from '$lib/utils/actions'; import { deleteAssets } from '$lib/utils/actions';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shortcuts, type ShortcutOptions } from '$lib/utils/shortcut';
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -49,19 +48,13 @@
const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
const onKeydown = (event: KeyboardEvent) => handlePromiseError(handleKeyboardPress(event));
onMount(async () => { onMount(async () => {
showSkeleton = false; showSkeleton = false;
document.addEventListener('keydown', onKeydown);
assetStore.connect(); assetStore.connect();
await assetStore.init(viewport); await assetStore.init(viewport);
}); });
onDestroy(() => { onDestroy(() => {
if (browser) {
document.removeEventListener('keydown', onKeydown);
}
if ($showAssetViewer) { if ($showAssetViewer) {
$showAssetViewer = false; $showAssetViewer = false;
} }
@ -75,51 +68,43 @@
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
}; };
const handleKeyboardPress = async (event: KeyboardEvent) => { const onDelete = () => {
if ($isSearchEnabled || shouldIgnoreShortcut(event)) { if (!isTrashEnabled && $showDeleteModal) {
isShowDeleteConfirmation = true;
return; return;
} }
handlePromiseError(trashOrDelete(false));
const key = event.key;
const shiftKey = event.shiftKey;
if (!$showAssetViewer) {
switch (key) {
case 'Escape': {
dispatch('escape');
return;
}
case '?': {
if (event.shiftKey) {
event.preventDefault();
showShortcuts = !showShortcuts;
}
return;
}
case '/': {
event.preventDefault();
await goto(AppRoute.EXPLORE);
return;
}
case 'Delete': {
if ($isMultiSelectState) {
let force = false;
if (shiftKey || !isTrashEnabled) {
if ($showDeleteModal) {
isShowDeleteConfirmation = true;
return;
}
force = true;
}
await trashOrDelete(force);
}
return;
}
}
}
}; };
const onForceDelete = () => {
if ($showDeleteModal) {
isShowDeleteConfirmation = true;
return;
}
handlePromiseError(trashOrDelete(true));
};
$: shortcutList = (() => {
if ($isSearchEnabled || $showAssetViewer) {
return [];
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
];
if ($isMultiSelectState) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
);
}
return shortcuts;
})();
const handleSelectAsset = (asset: AssetResponseDto) => { const handleSelectAsset = (asset: AssetResponseDto) => {
if (!assetStore.albumAssets.has(asset.id)) { if (!assetStore.albumAssets.has(asset.id)) {
assetInteractionStore.selectAsset(asset); assetInteractionStore.selectAsset(asset);
@ -371,7 +356,7 @@
}; };
</script> </script>
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} /> <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} use:shortcuts={shortcutList} />
{#if isShowDeleteConfirmation} {#if isShowDeleteConfirmation}
<DeleteAssetDialog <DeleteAssetDialog

View file

@ -1,4 +1,5 @@
import type { ActionReturn } from 'svelte/action'; import type { ActionReturn } from 'svelte/action';
import { matchesShortcut } from './shortcut';
interface Attributes { interface Attributes {
/** @deprecated */ /** @deprecated */
@ -29,7 +30,7 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
}; };
const handleKey = (event: KeyboardEvent) => { const handleKey = (event: KeyboardEvent) => {
if (event.key !== 'Escape') { if (!matchesShortcut(event, { key: 'Escape' })) {
return; return;
} }

View file

@ -1,7 +1,76 @@
export const shouldIgnoreShortcut = (event: Event): boolean => { import type { ActionReturn } from 'svelte/action';
const type = (event.target as HTMLInputElement).type;
if (['textarea', 'text'].includes(type)) { export type Shortcut = {
return true; key: string;
} alt?: boolean;
return false; ctrl?: boolean;
shift?: boolean;
meta?: boolean;
};
export type ShortcutOptions<T = HTMLElement> = {
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 = <T extends HTMLElement>(
node: T,
option: ShortcutOptions<T>,
): ActionReturn<ShortcutOptions<T>> => {
const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]);
return {
update(newOption) {
shortcutsUpdate?.([newOption]);
},
destroy,
};
};
export const shortcuts = <T extends HTMLElement>(
node: T,
options: ShortcutOptions<T>[],
): ActionReturn<ShortcutOptions<T>[]> => {
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);
},
};
}; };

View file

@ -113,7 +113,6 @@
let reactions: ActivityResponseDto[] = []; let reactions: ActivityResponseDto[] = [];
let globalWidth: number; let globalWidth: number;
let assetGridWidth: number; let assetGridWidth: number;
let textArea: HTMLTextAreaElement;
let albumOrder: AssetOrder | undefined = data.album.order; let albumOrder: AssetOrder | undefined = data.album.order;
$: assetStore = new AssetStore({ albumId, order: albumOrder }); $: assetStore = new AssetStore({ albumId, order: albumOrder });
@ -225,20 +224,6 @@
handlePromiseError(getNumberOfComments()); 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 handleStartSlideshow = async () => {
const asset = const asset =
$slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0]; $slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
@ -394,8 +379,6 @@
}; };
</script> </script>
<svelte:window on:keydown={handleKeypress} />
<div class="flex overflow-hidden" bind:clientWidth={globalWidth}> <div class="flex overflow-hidden" bind:clientWidth={globalWidth}>
<div class="relative w-full shrink"> <div class="relative w-full shrink">
{#if $isMultiSelectState} {#if $isMultiSelectState}

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
@ -27,7 +26,7 @@
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { searchNameLocal } from '$lib/utils/person'; import { searchNameLocal } from '$lib/utils/person';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shortcut } from '$lib/utils/shortcut';
import { import {
getPerson, getPerson,
mergePerson, mergePerson,
@ -38,7 +37,7 @@
type PersonResponseDto, type PersonResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js'; import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
@ -79,10 +78,7 @@
$: countVisiblePeople = countTotalPeople - countHiddenPeople; $: countVisiblePeople = countTotalPeople - countHiddenPeople;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(async () => { onMount(async () => {
document.addEventListener('keydown', onKeyboardPress);
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE); const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
if (getSearchedPeople) { if (getSearchedPeople) {
searchName = getSearchedPeople; searchName = getSearchedPeople;
@ -90,24 +86,6 @@
} }
}); });
onDestroy(() => {
if (browser) {
document.removeEventListener('keydown', onKeyboardPress);
}
});
const handleKeyboardPress = (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) {
return;
}
switch (event.key) {
case 'Escape': {
handleCloseClick();
return;
}
}
};
const handleSearch = async (force: boolean) => { const handleSearch = async (force: boolean) => {
$page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName); $page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName);
await goto($page.url); await goto($page.url);
@ -417,7 +395,7 @@
}; };
</script> </script>
<svelte:window bind:innerHeight /> <svelte:window bind:innerHeight use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: handleCloseClick }} />
{#if showMergeModal} {#if showMergeModal}
<MergeSuggestionModal <MergeSuggestionModal

View file

@ -20,7 +20,7 @@
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { preventRaceConditionSearchBar } from '$lib/stores/search.store'; import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shortcut } from '$lib/utils/shortcut';
import { import {
type AssetResponseDto, type AssetResponseDto,
searchSmart, searchSmart,
@ -52,27 +52,19 @@
let searchResultAssets: AssetResponseDto[] = []; let searchResultAssets: AssetResponseDto[] = [];
let isLoading = true; let isLoading = true;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const onEscape = () => {
if ($showAssetViewer) {
const handleKeyboardPress = async (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) {
return; return;
} }
if (!$showAssetViewer) {
switch (event.key) { if (isMultiSelectionMode) {
case 'Escape': { selectedAssets = new Set();
if (isMultiSelectionMode) { return;
selectedAssets = new Set();
return;
}
if (!$preventRaceConditionSearchBar) {
await goto(previousRoute);
}
$preventRaceConditionSearchBar = false;
return;
}
}
} }
if (!$preventRaceConditionSearchBar) {
handlePromiseError(goto(previousRoute));
}
$preventRaceConditionSearchBar = false;
}; };
afterNavigate(({ from }) => { afterNavigate(({ from }) => {
@ -201,7 +193,7 @@
} }
</script> </script>
<svelte:document on:keydown={onKeyboardPress} /> <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} />
<section> <section>
{#if isMultiSelectionMode} {#if isMultiSelectionMode}