From f21fe8716c1033f22a2fa82e4c4a1db1e6d9982d Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 9 May 2025 19:24:36 +0200 Subject: [PATCH] refactor: shortcuts modal (#18175) --- .../components/photos-page/asset-grid.svelte | 37 ++++--- .../gallery-viewer/gallery-viewer.svelte | 38 ++++--- .../shared-components/show-shortcuts.svelte | 94 ---------------- web/src/lib/modals/ShortcutsModal.svelte | 100 ++++++++++++++++++ .../routes/(user)/user-settings/+page.svelte | 15 +-- .../[[assetId=id]]/+page.svelte | 9 +- 6 files changed, 153 insertions(+), 140 deletions(-) delete mode 100644 web/src/lib/components/shared-components/show-shortcuts.svelte create mode 100644 web/src/lib/modals/ShortcutsModal.svelte diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 5b48f0494f..ba021b8ad1 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,33 +1,34 @@ <script lang="ts"> import { afterNavigate, beforeNavigate, goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; import type { Action } from '$lib/components/asset-viewer/actions/action'; + import Skeleton from '$lib/components/photos-page/skeleton.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; + import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetBucket, assetsSnapshot, AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; + import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { searchStore } from '$lib/stores/search.svelte'; import { featureFlags } from '$lib/stores/server-config.store'; import { handlePromiseError } from '$lib/utils'; import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; + import { focusNext } from '$lib/utils/focus-util'; import { navigate } from '$lib/utils/navigation'; import { type ScrubberListener } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk'; import { onMount, type Snippet } from 'svelte'; + import type { UpdatePayload } from 'vite'; import Portal from '../shared-components/portal/portal.svelte'; import Scrubber from '../shared-components/scrubber/scrubber.svelte'; - import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; - import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; - import Skeleton from '$lib/components/photos-page/skeleton.svelte'; - import { page } from '$app/stores'; - import type { UpdatePayload } from 'vite'; - import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { mobileDevice } from '$lib/stores/mobile-device.svelte'; - import { focusNext } from '$lib/utils/focus-util'; - import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte'; interface Props { isSelectionMode?: boolean; @@ -75,7 +76,6 @@ let element: HTMLElement | undefined = $state(); let timelineElement: HTMLElement | undefined = $state(); - let showShortcuts = $state(false); let showSkeleton = $state(true); let scrubBucketPercent = $state(0); let scrubBucket: { bucketDate: string | undefined } | undefined = $state(); @@ -623,6 +623,17 @@ let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isEmpty = $derived(assetStore.isInitialized && assetStore.buckets.length === 0); let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id)); + let isShortcutModalOpen = false; + + const handleOpenShortcutModal = async () => { + if (isShortcutModalOpen) { + return; + } + + isShortcutModalOpen = true; + await modalManager.show(ShortcutsModal, {}); + isShortcutModalOpen = false; + }; $effect(() => { if (isEmpty) { @@ -638,7 +649,7 @@ const shortcuts: ShortcutOptions[] = [ { shortcut: { key: 'Escape' }, onShortcut: onEscape }, - { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, + { shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) }, { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, @@ -690,10 +701,6 @@ /> {/if} -{#if showShortcuts} - <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} /> -{/if} - {#if assetStore.buckets.length > 0} <Scrubber {assetStore} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 09c126e0df..b820868b59 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -1,28 +1,29 @@ <script lang="ts"> - import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut'; import { goto } from '$app/navigation'; + import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; import type { Action } from '$lib/components/asset-viewer/actions/action'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import type { Viewport } from '$lib/stores/assets-store.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; + import { featureFlags } from '$lib/stores/server-config.store'; + import { handlePromiseError } from '$lib/utils'; import { deleteAssets } from '$lib/utils/actions'; import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils'; - import { featureFlags } from '$lib/stores/server-config.store'; + import { focusNext } from '$lib/utils/focus-util'; import { handleError } from '$lib/utils/handle-error'; + import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils'; import { navigate } from '$lib/utils/navigation'; import { type AssetResponseDto } from '@immich/sdk'; + import { debounce } from 'lodash-es'; import { t } from 'svelte-i18n'; import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; - import ShowShortcuts from '../show-shortcuts.svelte'; - import Portal from '../portal/portal.svelte'; - import { handlePromiseError } from '$lib/utils'; import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; - import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { debounce } from 'lodash-es'; - import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils'; - import { focusNext } from '$lib/utils/focus-util'; + import Portal from '../portal/portal.svelte'; interface Props { assets: AssetResponseDto[]; @@ -106,7 +107,6 @@ }; }); - let showShortcuts = $state(false); let currentViewAssetIndex = 0; let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: AssetResponseDto | null = $state(null); @@ -263,6 +263,18 @@ const focusNextAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, true); const focusPreviousAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, false); + let isShortcutModalOpen = false; + + const handleOpenShortcutModal = async () => { + if (isShortcutModalOpen) { + return; + } + + isShortcutModalOpen = true; + await modalManager.show(ShortcutsModal, {}); + isShortcutModalOpen = false; + }; + let shortcutList = $derived( (() => { if ($isViewerOpen) { @@ -270,7 +282,7 @@ } const shortcuts: ShortcutOptions[] = [ - { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, + { shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, { shortcut: { key: 'ArrowRight' }, preventDefault: false, onShortcut: focusNextAsset }, @@ -439,10 +451,6 @@ /> {/if} -{#if showShortcuts} - <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} /> -{/if} - {#if assets.length > 0} <div style:position="relative" diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte deleted file mode 100644 index e1454b49df..0000000000 --- a/web/src/lib/components/shared-components/show-shortcuts.svelte +++ /dev/null @@ -1,94 +0,0 @@ -<script lang="ts"> - import { mdiInformationOutline } from '@mdi/js'; - import { t } from 'svelte-i18n'; - import Icon from '../elements/icon.svelte'; - import FullScreenModal from './full-screen-modal.svelte'; - - interface Shortcuts { - general: ExplainedShortcut[]; - actions: ExplainedShortcut[]; - } - - interface ExplainedShortcut { - key: string[]; - action: string; - info?: string; - } - - interface Props { - onClose: () => void; - shortcuts?: Shortcuts; - } - - let { - onClose, - shortcuts = { - general: [ - { key: ['←', '→'], action: $t('previous_or_next_photo') }, - { key: ['x'], action: $t('select') }, - { key: ['Esc'], action: $t('back_close_deselect') }, - { key: ['Ctrl', 'k'], action: $t('search_your_photos') }, - { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') }, - ], - actions: [ - { key: ['f'], action: $t('favorite_or_unfavorite_photo') }, - { key: ['i'], action: $t('show_or_hide_info') }, - { key: ['s'], action: $t('stack_selected_photos') }, - { key: ['l'], action: $t('add_to_album') }, - { key: ['⇧', 'l'], action: $t('add_to_shared_album') }, - { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') }, - { key: ['⇧', 'd'], action: $t('download') }, - { key: ['Space'], action: $t('play_or_pause_video') }, - { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') }, - ], - }, - }: Props = $props(); -</script> - -<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" {onClose}> - <div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2"> - {#if shortcuts.general.length > 0} - <div class="p-4"> - <h2>{$t('general')}</h2> - <div class="text-sm"> - {#each shortcuts.general as shortcut (shortcut.key.join('-'))} - <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm"> - <div class="flex justify-self-end"> - {#each shortcut.key as key (key)} - <p class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"> - {key} - </p> - {/each} - </div> - <p class="mb-1 mt-1 flex">{shortcut.action}</p> - </div> - {/each} - </div> - </div> - {/if} - {#if shortcuts.actions.length > 0} - <div class="p-4"> - <h2>{$t('actions')}</h2> - <div class="text-sm"> - {#each shortcuts.actions as shortcut (shortcut.key.join('-'))} - <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm"> - <div class="flex justify-self-end"> - {#each shortcut.key as key (key)} - <p class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"> - {key} - </p> - {/each} - </div> - <div class="flex items-center gap-2"> - <p class="mb-1 mt-1 flex">{shortcut.action}</p> - {#if shortcut.info} - <Icon path={mdiInformationOutline} title={shortcut.info} /> - {/if} - </div> - </div> - {/each} - </div> - </div> - {/if} - </div> -</FullScreenModal> diff --git a/web/src/lib/modals/ShortcutsModal.svelte b/web/src/lib/modals/ShortcutsModal.svelte new file mode 100644 index 0000000000..2f16eaa817 --- /dev/null +++ b/web/src/lib/modals/ShortcutsModal.svelte @@ -0,0 +1,100 @@ +<script lang="ts"> + import { Modal, ModalBody } from '@immich/ui'; + import { mdiInformationOutline } from '@mdi/js'; + import { t } from 'svelte-i18n'; + import Icon from '../components/elements/icon.svelte'; + + interface Shortcuts { + general: ExplainedShortcut[]; + actions: ExplainedShortcut[]; + } + + interface ExplainedShortcut { + key: string[]; + action: string; + info?: string; + } + + interface Props { + onClose: () => void; + shortcuts?: Shortcuts; + } + + let { + onClose, + shortcuts = { + general: [ + { key: ['←', '→'], action: $t('previous_or_next_photo') }, + { key: ['x'], action: $t('select') }, + { key: ['Esc'], action: $t('back_close_deselect') }, + { key: ['Ctrl', 'k'], action: $t('search_your_photos') }, + { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') }, + ], + actions: [ + { key: ['f'], action: $t('favorite_or_unfavorite_photo') }, + { key: ['i'], action: $t('show_or_hide_info') }, + { key: ['s'], action: $t('stack_selected_photos') }, + { key: ['l'], action: $t('add_to_album') }, + { key: ['⇧', 'l'], action: $t('add_to_shared_album') }, + { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') }, + { key: ['⇧', 'd'], action: $t('download') }, + { key: ['Space'], action: $t('play_or_pause_video') }, + { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') }, + ], + }, + }: Props = $props(); +</script> + +<Modal title={$t('keyboard_shortcuts')} size="medium" {onClose}> + <ModalBody> + <div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2"> + {#if shortcuts.general.length > 0} + <div class="p-4"> + <h2>{$t('general')}</h2> + <div class="text-sm"> + {#each shortcuts.general as shortcut (shortcut.key.join('-'))} + <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm"> + <div class="flex justify-self-end"> + {#each shortcut.key as key (key)} + <p + class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2" + > + {key} + </p> + {/each} + </div> + <p class="mb-1 mt-1 flex">{shortcut.action}</p> + </div> + {/each} + </div> + </div> + {/if} + {#if shortcuts.actions.length > 0} + <div class="p-4"> + <h2>{$t('actions')}</h2> + <div class="text-sm"> + {#each shortcuts.actions as shortcut (shortcut.key.join('-'))} + <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm"> + <div class="flex justify-self-end"> + {#each shortcut.key as key (key)} + <p + class="me-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2" + > + {key} + </p> + {/each} + </div> + <div class="flex items-center gap-2"> + <p class="mb-1 mt-1 flex">{shortcut.action}</p> + {#if shortcut.info} + <Icon path={mdiInformationOutline} title={shortcut.info} /> + {/if} + </div> + </div> + {/each} + </div> + </div> + {/if} + </div> + </ModalBody> +</Modal> diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index 54d54f5b40..028941cdd6 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -1,19 +1,18 @@ <script lang="ts"> + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import { mdiKeyboard } from '@mdi/js'; - import type { PageData } from './$types'; - import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; + import type { PageData } from './$types'; interface Props { data: PageData; } let { data }: Props = $props(); - - let isShowKeyboardShortcut = $state(false); </script> <UserPageLayout title={data.meta.title}> @@ -21,7 +20,7 @@ <CircleIconButton icon={mdiKeyboard} title={$t('show_keyboard_shortcuts')} - onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)} + onclick={() => modalManager.show(ShortcutsModal, {})} /> {/snippet} <section class="mx-4 flex place-content-center"> @@ -30,7 +29,3 @@ </div> </section> </UserPageLayout> - -{#if isShowKeyboardShortcut} - <ShowShortcuts onClose={() => (isShowKeyboardShortcut = false)} /> -{/if} diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6c21260f99..14b1420110 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -7,8 +7,9 @@ NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; - import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { stackAssets } from '$lib/utils/asset-utils'; @@ -27,7 +28,6 @@ let { data = $bindable() }: Props = $props(); - let isShowKeyboardShortcut = $state(false); let isShowDuplicateInfo = $state(false); interface Shortcuts { @@ -185,7 +185,7 @@ color="secondary" icon={mdiKeyboard} title={$t('show_keyboard_shortcuts')} - onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)} + onclick={() => modalManager.show(ShortcutsModal, { shortcuts: duplicateShortcuts })} aria-label={$t('show_keyboard_shortcuts')} /> </HStack> @@ -222,9 +222,6 @@ </div> </UserPageLayout> -{#if isShowKeyboardShortcut} - <ShowShortcuts shortcuts={duplicateShortcuts} onClose={() => (isShowKeyboardShortcut = false)} /> -{/if} {#if isShowDuplicateInfo} <DuplicatesModal onClose={() => (isShowDuplicateInfo = false)} /> {/if}