diff --git a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts index c94340484b..4f20e2db19 100644 --- a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts @@ -10,6 +10,9 @@ test.describe('Asset Viewer Navbar', () => { utils.initSdk(); await utils.resetDatabase(); admin = await utils.adminSetup(); + }); + + test.beforeEach(async () => { asset = await utils.createAsset(admin.accessToken); }); @@ -49,4 +52,14 @@ test.describe('Asset Viewer Navbar', () => { } }); }); + + test.describe('actions', () => { + test('favorite asset with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list').getByTestId('message')).toHaveText('Added to favorites'); + }); + }); }); diff --git a/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts new file mode 100644 index 0000000000..72bb3c5c59 --- /dev/null +++ b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts @@ -0,0 +1,56 @@ +import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; +import { expect, type Page, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Slideshow', () => { + let admin: LoginResponseDto; + let asset: AssetMediaResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + asset = await utils.createAsset(admin.accessToken); + }); + + const openSlideshow = async (page: Page) => { + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.getByRole('button', { name: 'More' }).click(); + await page.getByRole('menuitem', { name: 'Slideshow' }).click(); + }; + + test('open slideshow', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + }); + + test('exit slideshow with button', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await exitButton.click(); + await expect(exitButton).not.toBeVisible(); + }); + + test('exit slideshow with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await expect(exitButton).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(exitButton).not.toBeVisible(); + }); + + test('favorite shortcut is disabled', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list')).not.toBeVisible(); + }); +}); diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts new file mode 100644 index 0000000000..d6136f2d18 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -0,0 +1,20 @@ +import type { AssetAction } from '$lib/constants'; +import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; + +type ActionMap = { + [AssetAction.ARCHIVE]: { asset: AssetResponseDto }; + [AssetAction.UNARCHIVE]: { asset: AssetResponseDto }; + [AssetAction.FAVORITE]: { asset: AssetResponseDto }; + [AssetAction.UNFAVORITE]: { asset: AssetResponseDto }; + [AssetAction.TRASH]: { asset: AssetResponseDto }; + [AssetAction.DELETE]: { asset: AssetResponseDto }; + [AssetAction.RESTORE]: { asset: AssetResponseDto }; + [AssetAction.ADD]: { asset: AssetResponseDto }; + [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto }; + [AssetAction.UNSTACK]: { assets: AssetResponseDto[] }; +}; + +export type Action = { + [K in AssetAction]: { type: K } & ActionMap[K]; +}[AssetAction]; +export type OnAction = (action: Action) => void; diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte new file mode 100644 index 0000000000..15d3b6accc --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -0,0 +1,48 @@ + + + (showSelectionModal = true)} +/> + +{#if showSelectionModal} + + handleAddToNewAlbum(detail)} + on:album={({ detail }) => handleAddToAlbum(detail)} + onClose={() => (showSelectionModal = false)} + /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte new file mode 100644 index 0000000000..3e2c453f39 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/close-action.svelte b/web/src/lib/components/asset-viewer/actions/close-action.svelte new file mode 100644 index 0000000000..647ad61e4f --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/close-action.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/delete-button.spec.ts b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts similarity index 72% rename from web/src/lib/components/asset-viewer/delete-button.spec.ts rename to web/src/lib/components/asset-viewer/actions/delete-action.spec.ts index 7d14a86ab2..e0b33ff48b 100644 --- a/web/src/lib/components/asset-viewer/delete-button.spec.ts +++ b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts @@ -1,20 +1,19 @@ -import { type AssetResponseDto } from '@immich/sdk'; - +import type { AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; import '@testing-library/jest-dom'; import { render } from '@testing-library/svelte'; -import DeleteButton from './delete-button.svelte'; +import DeleteAction from './delete-action.svelte'; let asset: AssetResponseDto; -describe('DeleteButton component', () => { +describe('DeleteAction component', () => { describe('given an asset which is not trashed yet', () => { beforeEach(() => { asset = assetFactory.build({ isTrashed: false }); }); it('displays a button to move the asset to the trash bin', () => { - const { getByTitle, queryByTitle } = render(DeleteButton, { asset }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); expect(getByTitle('delete')).toBeInTheDocument(); expect(queryByTitle('deletePermanently')).toBeNull(); }); @@ -26,7 +25,7 @@ describe('DeleteButton component', () => { }); it('displays a button to permanently delete the asset', () => { - const { getByTitle, queryByTitle } = render(DeleteButton, { asset }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); expect(getByTitle('permanently_delete')).toBeInTheDocument(); expect(queryByTitle('delete')).toBeNull(); }); diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte new file mode 100644 index 0000000000..1e3cfdd28d --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -0,0 +1,87 @@ + + + trashOrDelete(asset.isTrashed) }, + { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, + ]} +/> + + trashOrDelete(asset.isTrashed)} +/> + +{#if showConfirmModal} + + (showConfirmModal = false)} on:confirm={() => deleteAsset()} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte new file mode 100644 index 0000000000..88c0eeadf2 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -0,0 +1,22 @@ + + + + +{#if !menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte new file mode 100644 index 0000000000..488ed7ecb2 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte @@ -0,0 +1,47 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte new file mode 100644 index 0000000000..fd519a05d4 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte @@ -0,0 +1,15 @@ + + + onClick(!isPlaying)} +/> diff --git a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte new file mode 100644 index 0000000000..a4ee322996 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte new file mode 100644 index 0000000000..ef836b618c --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/restore-action.svelte b/web/src/lib/components/asset-viewer/actions/restore-action.svelte new file mode 100644 index 0000000000..c000dad9a1 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/restore-action.svelte @@ -0,0 +1,34 @@ + + + diff --git a/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte new file mode 100644 index 0000000000..f20c4872bc --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte @@ -0,0 +1,34 @@ + + + diff --git a/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte new file mode 100644 index 0000000000..23c147815c --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte @@ -0,0 +1,24 @@ + + + (showProfileImageCrop = true)} + text={$t('set_as_profile_picture')} +/> + +{#if showProfileImageCrop} + + (showProfileImageCrop = false)} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte new file mode 100644 index 0000000000..f0b2177128 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -0,0 +1,25 @@ + + + (showModal = true)} + title={$t('share')} +/> + +{#if showModal} + + (showModal = false)} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte new file mode 100644 index 0000000000..66e5d0e10f --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte new file mode 100644 index 0000000000..40178c472d --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte @@ -0,0 +1,21 @@ + + + diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index fc1239d396..85eff91ff4 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -1,135 +1,80 @@
- dispatch('back')} /> +
- {#if showShareButton} - dispatch('showShareModal')} - title={$t('share')} - /> + {#if !asset.isTrashed && $user} + {/if} {#if asset.isOffline} - dispatch('showDetail')} - title={$t('asset_offline')} - /> + {/if} - {#if showMotionPlayButton} - {#if isMotionPhotoPlaying} - dispatch('stopMotionPhoto')} - /> - {:else} - dispatch('playMotionPhoto')} - /> - {/if} + {#if asset.livePhotoVideoId} + {/if} - {#if showZoomButton} + {#if asset.type === AssetTypeEnum.Image} {/if} - {#if showCopyButton} + {#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image} {/if} {#if !isOwner && showDownloadButton} - dispatch('download')} - title={$t('download')} - /> + {/if} {#if showDetailButton} - dispatch('showDetail')} - title={$t('info')} - /> + {/if} {#if isOwner} - dispatch('favorite')} - title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')} - /> + {/if} {#if isOwner} - dispatch('delete')} - on:permanentlyDelete={() => dispatch('permanentlyDelete')} - /> + + {#if showSlideshow} - onMenuClick('playSlideShow')} text={$t('slideshow')} /> + {/if} {#if showDownloadButton} - onMenuClick('download')} text={$t('download')} /> + {/if} {#if asset.isTrashed} - onMenuClick('restoreAsset')} text={$t('restore')} /> + {:else} - onMenuClick('addToAlbum')} text={$t('add_to_album')} /> - onMenuClick('addToSharedAlbum')} - text={$t('add_to_shared_album')} - /> + + {/if} {#if isOwner} {#if hasStackChildren} - onMenuClick('unstack')} text={$t('unstack')} /> + {/if} {#if album} - onMenuClick('setAsAlbumCover')} - /> + {/if} {#if asset.type === AssetTypeEnum.Image} - onMenuClick('asProfileImage')} - text={$t('set_as_profile_picture')} - /> + {/if} - onMenuClick('toggleArchive')} - icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} - text={asset.isArchived ? $t('unarchive') : $t('to_archive')} - /> + openFileUploadDialog({ multiple: false, assetId: asset.id })} @@ -224,18 +135,18 @@
onJobClick(AssetJobName.RefreshMetadata)} + onClick={() => onRunJob(AssetJobName.RefreshMetadata)} text={$getAssetJobName(AssetJobName.RefreshMetadata)} /> onJobClick(AssetJobName.RegenerateThumbnail)} + onClick={() => onRunJob(AssetJobName.RegenerateThumbnail)} text={$getAssetJobName(AssetJobName.RegenerateThumbnail)} /> {#if asset.type === AssetTypeEnum.Video} onJobClick(AssetJobName.TranscodeVideo)} + onClick={() => onRunJob(AssetJobName.TranscodeVideo)} text={$getAssetJobName(AssetJobName.TranscodeVideo)} /> {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f216d73382..24b65f8b1b 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,25 +1,21 @@ - navigateAsset('previous') }, - { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, - { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) }, - { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, - { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, - { shortcut: { key: 'f' }, onShortcut: toggleFavorite }, - { shortcut: { key: 'i' }, onShortcut: toggleDetailPanel }, - ]} -/> -
0} - showShareButton={shouldShowShareModal} + hasStackChildren={stackedAssets.length > 0} onZoomImage={zoomToggle} onCopyImage={copyImage} - on:back={closeViewer} - on:showDetail={showDetailInfoHandler} - on:download={() => downloadFile(asset)} - on:delete={() => trashOrDelete()} - on:permanentlyDelete={() => trashOrDelete(true)} - on:favorite={toggleFavorite} - on:addToAlbum={() => openAlbumPicker(false)} - on:restoreAsset={() => handleRestoreAsset()} - on:addToSharedAlbum={() => openAlbumPicker(true)} - on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} - on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} - on:toggleArchive={toggleAssetArchive} - on:asProfileImage={() => (isShowProfileImageCrop = true)} - on:setAsAlbumCover={handleUpdateThumbnail} - on:runJob={({ detail: job }) => handleRunJob(job)} - on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)} - on:unstack={handleUnstack} - on:showShareModal={() => (isShowShareModal = true)} - /> + onAction={handleAction} + onRunJob={handleRunJob} + onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} + onShowDetail={toggleDetailPanel} + onClose={closeViewer} + > + (shouldPlayMotionPhoto = shouldPlay)} + /> +
{/if} {#if $slideshowState === SlideshowState.None && showNavigation}
- navigateAsset('previous', e)} label={$t('view_previous_asset')}> - - + navigateAsset('previous')} />
{/if} @@ -698,9 +505,7 @@ {#if $slideshowState === SlideshowState.None && showNavigation}
- navigateAsset('next', e)} label={$t('view_next_asset')}> - - + navigateAsset('next')} />
{/if} @@ -715,13 +520,13 @@
{/if} - {#if $stackAssetsStore.length > 0 && withStacked} + {#if stackedAssets.length > 0 && withStacked}
- {#each $stackAssetsStore as stackedAsset, index (stackedAsset.id)} + {#each stackedAssets as stackedAsset, index (stackedAsset.id)}
- import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { createEventDispatcher } from 'svelte'; - import { t } from 'svelte-i18n'; - import { mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js'; - import { type AssetResponseDto } from '@immich/sdk'; - - export let asset: AssetResponseDto; - - type EventTypes = { - delete: void; - permanentlyDelete: void; - }; - - const dispatch = createEventDispatcher(); - - -{#if asset.isTrashed} - dispatch('permanentlyDelete')} - title={$t('permanently_delete')} - /> -{:else} - dispatch('delete')} title={$t('delete')} /> -{/if} diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index 8faac7e8d1..63e501f6dd 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -1,12 +1,13 @@ - + {#if showControls}
import { goto } from '$app/navigation'; + import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; + import type { Action } from '$lib/components/asset-viewer/actions/action'; import { AppRoute, AssetAction } from '$lib/constants'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; @@ -7,8 +9,10 @@ import { locale, showDeleteModal } from '$lib/stores/preferences.store'; import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; + import { handlePromiseError } from '$lib/utils'; import { deleteAssets } from '$lib/utils/actions'; - import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut'; + import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; + import { navigate } from '$lib/utils/navigation'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; @@ -18,17 +22,18 @@ import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; - import { archiveAssets, stackAssets } from '$lib/utils/asset-utils'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; - import { handlePromiseError } from '$lib/utils'; - import { selectAllAssets } from '$lib/utils/asset-utils'; - import { navigate } from '$lib/utils/navigation'; export let isSelectionMode = false; export let singleSelect = false; export let assetStore: AssetStore; export let assetInteractionStore: AssetInteractionStore; - export let removeAction: AssetAction | null = null; + export let removeAction: + | AssetAction.UNARCHIVE + | AssetAction.ARCHIVE + | AssetAction.FAVORITE + | AssetAction.UNFAVORITE + | null = null; export let withStacked = false; export let showArchiveIcon = false; export let isShared = false; @@ -193,8 +198,8 @@ const handleClose = () => assetViewingStore.showAssetViewer(false); - const handleAction = async (action: AssetAction, asset: AssetResponseDto) => { - switch (action) { + const handleAction = async (action: Action) => { + switch (action.type) { case removeAction: case AssetAction.TRASH: case AssetAction.RESTORE: @@ -203,7 +208,7 @@ (await handleNext()) || (await handlePrevious()) || handleClose(); // delete after find the next one - assetStore.removeAssets([asset.id]); + assetStore.removeAssets([action.asset.id]); break; } @@ -211,14 +216,18 @@ case AssetAction.UNARCHIVE: case AssetAction.FAVORITE: case AssetAction.UNFAVORITE: { - assetStore.updateAssets([asset]); + assetStore.updateAssets([action.asset]); break; } case AssetAction.ADD: { - assetStore.addAssets([asset]); + assetStore.addAssets([action.asset]); break; } + + case AssetAction.UNSTACK: { + assetStore.addAssets(action.assets); + } } }; @@ -501,10 +510,10 @@ preloadAssets={$preloadAssets} {isShared} {album} + onAction={handleAction} on:previous={handlePrevious} on:next={handleNext} on:close={handleClose} - on:action={({ detail: action }) => handleAction(action.type, action.asset)} /> {/await} {/if} 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 819105e197..337b681a22 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,19 +1,20 @@