refactor(web): responsive asset viewer layout for portrait and landscape

This commit is contained in:
midzelis
2026-03-16 01:42:10 +00:00
parent 590a9df7ec
commit 0c29a76e67
3 changed files with 73 additions and 50 deletions

View File

@@ -447,6 +447,8 @@
!assetViewerManager.isShowEditor, !assetViewerManager.isShowEditor,
); );
const hasSidePanel = $derived(showDetailPanel || assetViewerManager.isShowEditor);
const onSwipe = (event: SwipeCustomEvent) => { const onSwipe = (event: SwipeCustomEvent) => {
if (assetViewerManager.zoom > 1) { if (assetViewerManager.zoom > 1) {
return; return;
@@ -473,7 +475,7 @@
<section <section
id="immich-asset-viewer" id="immich-asset-viewer"
class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black touch-none" class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] portrait:grid-rows-[64px_1fr_auto] overflow-hidden bg-black touch-none"
use:focusTrap use:focusTrap
bind:this={assetViewerHtmlElement} bind:this={assetViewerHtmlElement}
> >
@@ -511,13 +513,21 @@
{/if} {/if}
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && previousAsset} {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && previousAsset}
<div class="my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start"> <div
class={[
'my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start',
hasSidePanel && 'portrait:row-[1/3]',
]}
>
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} /> <PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
</div> </div>
{/if} {/if}
<!-- Asset Viewer --> <!-- Asset Viewer -->
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full"> <div
data-viewer-content
class={['z-[-1] relative col-start-1 col-span-4 row-span-full', hasSidePanel && 'portrait:row-[1/3]']}
>
{#if viewerKind === 'StackVideoViewer'} {#if viewerKind === 'StackVideoViewer'}
<VideoViewer <VideoViewer
asset={previewStackedAsset!} asset={previewStackedAsset!}
@@ -581,40 +591,21 @@
<OcrButton /> <OcrButton />
</div> </div>
{/if} {/if}
</div>
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && nextAsset}
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
</div>
{/if}
{#if showDetailPanel || assetViewerManager.isShowEditor}
<div
transition:fly={{ duration: 150 }}
id="detail-panel"
class="row-start-1 row-span-4 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
translate="yes"
>
{#if showDetailPanel}
<div class="w-90 h-full">
<DetailPanel {asset} currentAlbum={album} />
</div>
{:else if assetViewerManager.isShowEditor}
<div class="w-100 h-full">
<EditorPanel {asset} onClose={closeEditor} />
</div>
{/if}
</div>
{/if}
{#if stack && withStacked && !assetViewerManager.isShowEditor} {#if stack && withStacked && !assetViewerManager.isShowEditor}
{@const stackedAssets = stack.assets} {@const stackedAssets = stack.assets}
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none"> <div
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar"> id="stack-slideshow"
class="absolute bottom-0 max-w-[calc(100%-5rem)] col-span-4 col-start-1 pointer-events-none"
>
<div
role="presentation"
class="relative inline-flex flex-row flex-nowrap max-w-full overflow-x-auto overflow-y-hidden horizontal-scrollbar pointer-events-auto"
onmouseleave={() => (previewStackedAsset = undefined)}
>
{#each stackedAssets as stackedAsset (stackedAsset.id)} {#each stackedAssets as stackedAsset (stackedAsset.id)}
<div <div
class={['inline-block px-1 relative transition-all pb-2 pointer-events-auto']} class={['inline-block px-1 relative transition-all pb-2']}
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'} style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
> >
<Thumbnail <Thumbnail
@@ -634,16 +625,47 @@
disableLinkMouseOver disableLinkMouseOver
/> />
{#if stackedAsset.id === asset.id}
<div class="w-full flex place-items-center place-content-center"> <div class="w-full flex place-items-center place-content-center">
<div class="w-2 h-2 bg-white rounded-full flex mt-0.5"></div> <div class={['w-2 h-2 rounded-full flex mt-0.5', { 'bg-white': stackedAsset.id === asset.id }]}></div>
</div> </div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
{/if} {/if}
</div>
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && nextAsset}
<div
class={[
'my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end',
hasSidePanel && 'portrait:row-[1/3]',
]}
>
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
</div>
{/if}
{#if showDetailPanel || assetViewerManager.isShowEditor}
<div
transition:fly={{ duration: 150 }}
id="detail-panel"
class="overflow-y-auto transition-all bg-light
landscape:row-start-1 landscape:row-span-4 landscape:dark:border-l landscape:dark:border-s-immich-dark-gray
portrait:col-span-full portrait:row-start-3 portrait:max-h-[40dvh] portrait:dark:border-t portrait:dark:border-t-immich-dark-gray"
translate="yes"
>
{#if showDetailPanel}
<div class="relative portrait:w-full landscape:w-[min(22.5rem,30vw)] landscape:min-w-56 h-full">
<DetailPanel {asset} currentAlbum={album} />
</div>
{:else if assetViewerManager.isShowEditor}
<div class="landscape:w-[min(25rem,30vw)] landscape:min-w-56 h-full">
<EditorPanel {asset} onClose={closeEditor} />
</div>
{/if}
</div>
{/if}
{#if isShared && album && assetViewerManager.isShowActivityPanel && $user} {#if isShared && album && assetViewerManager.isShowActivityPanel && $user}
<div <div

View File

@@ -340,7 +340,7 @@
</div> </div>
{#if isOwner} {#if isOwner}
<div class="p-1"> <div class="shrink-0 p-1">
<Icon icon={mdiPencil} size="20" /> <Icon icon={mdiPencil} size="20" />
</div> </div>
{/if} {/if}
@@ -352,20 +352,21 @@
<Icon icon={mdiCalendar} size="24" /> <Icon icon={mdiCalendar} size="24" />
</div> </div>
</div> </div>
<div class="p-1"> <div class="shrink-0 p-1">
<Icon icon={mdiPencil} size="20" /> <Icon icon={mdiPencil} size="20" />
</div> </div>
</div> </div>
{/if} {/if}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div><Icon icon={mdiImageOutline} size="24" /></div> <div class="shrink-0"><Icon icon={mdiImageOutline} size="24" /></div>
<div> <div>
<p class="break-all flex place-items-center gap-2 whitespace-pre-wrap"> <p class="break-all flex place-items-center gap-2 whitespace-pre-wrap">
{asset.originalFileName} {asset.originalFileName}
{#if isOwner} {#if isOwner}
<IconButton <IconButton
class="shrink-0"
icon={mdiInformationOutline} icon={mdiInformationOutline}
aria-label={$t('show_file_location')} aria-label={$t('show_file_location')}
size="small" size="small"
@@ -405,7 +406,7 @@
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso} {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div><Icon icon={mdiCamera} size="24" /></div> <div class="shrink-0"><Icon icon={mdiCamera} size="24" /></div>
<div> <div>
{#if asset.exifInfo?.make || asset.exifInfo?.model} {#if asset.exifInfo?.make || asset.exifInfo?.model}
@@ -439,7 +440,7 @@
{#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength} {#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div><Icon icon={mdiCameraIris} size="24" /></div> <div class="shrink-0"><Icon icon={mdiCameraIris} size="24" /></div>
<div> <div>
{#if asset.exifInfo?.lensModel} {#if asset.exifInfo?.lensModel}

View File

@@ -192,7 +192,7 @@
<section <section
transition:fly={{ x: 360, duration: 100, easing: linear }} transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 h-full w-90 overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light" class="absolute top-0 h-full w-full overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
> >
<div class="flex place-items-center justify-between gap-2"> <div class="flex place-items-center justify-between gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">