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:
parent
12fb90c232
commit
eed8e6b67a
14 changed files with 197 additions and 268 deletions
|
@ -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">
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue