mirror of
https://github.com/immich-app/immich.git
synced 2026-03-26 11:50:53 +03:00
refactor(web): responsive asset viewer layout for portrait and landscape
This commit is contained in:
@@ -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,10 +591,57 @@
|
|||||||
<OcrButton />
|
<OcrButton />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if stack && withStacked && !assetViewerManager.isShowEditor}
|
||||||
|
{@const stackedAssets = stack.assets}
|
||||||
|
<div
|
||||||
|
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)}
|
||||||
|
<div
|
||||||
|
class={['inline-block px-1 relative transition-all pb-2']}
|
||||||
|
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
|
||||||
|
>
|
||||||
|
<Thumbnail
|
||||||
|
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
|
||||||
|
brokenAssetClass="text-xs"
|
||||||
|
dimmed={stackedAsset.id !== asset.id}
|
||||||
|
asset={toTimelineAsset(stackedAsset)}
|
||||||
|
onClick={() => {
|
||||||
|
cursor.current = stackedAsset;
|
||||||
|
previewStackedAsset = undefined;
|
||||||
|
isFaceEditMode.value = false;
|
||||||
|
}}
|
||||||
|
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
|
||||||
|
readonly
|
||||||
|
thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize}
|
||||||
|
showStackedIcon={false}
|
||||||
|
disableLinkMouseOver
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full flex place-items-center place-content-center">
|
||||||
|
<div class={['w-2 h-2 rounded-full flex mt-0.5', { 'bg-white': stackedAsset.id === asset.id }]}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && nextAsset}
|
{#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">
|
<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')} />
|
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -593,58 +650,23 @@
|
|||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
id="detail-panel"
|
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"
|
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"
|
translate="yes"
|
||||||
>
|
>
|
||||||
{#if showDetailPanel}
|
{#if showDetailPanel}
|
||||||
<div class="w-90 h-full">
|
<div class="relative portrait:w-full landscape:w-[min(22.5rem,30vw)] landscape:min-w-56 h-full">
|
||||||
<DetailPanel {asset} currentAlbum={album} />
|
<DetailPanel {asset} currentAlbum={album} />
|
||||||
</div>
|
</div>
|
||||||
{:else if assetViewerManager.isShowEditor}
|
{:else if assetViewerManager.isShowEditor}
|
||||||
<div class="w-100 h-full">
|
<div class="landscape:w-[min(25rem,30vw)] landscape:min-w-56 h-full">
|
||||||
<EditorPanel {asset} onClose={closeEditor} />
|
<EditorPanel {asset} onClose={closeEditor} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if stack && withStacked && !assetViewerManager.isShowEditor}
|
|
||||||
{@const stackedAssets = stack.assets}
|
|
||||||
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none">
|
|
||||||
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
|
|
||||||
{#each stackedAssets as stackedAsset (stackedAsset.id)}
|
|
||||||
<div
|
|
||||||
class={['inline-block px-1 relative transition-all pb-2 pointer-events-auto']}
|
|
||||||
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
|
|
||||||
>
|
|
||||||
<Thumbnail
|
|
||||||
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
|
|
||||||
brokenAssetClass="text-xs"
|
|
||||||
dimmed={stackedAsset.id !== asset.id}
|
|
||||||
asset={toTimelineAsset(stackedAsset)}
|
|
||||||
onClick={() => {
|
|
||||||
cursor.current = stackedAsset;
|
|
||||||
previewStackedAsset = undefined;
|
|
||||||
isFaceEditMode.value = false;
|
|
||||||
}}
|
|
||||||
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
|
|
||||||
readonly
|
|
||||||
thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize}
|
|
||||||
showStackedIcon={false}
|
|
||||||
disableLinkMouseOver
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if stackedAsset.id === asset.id}
|
|
||||||
<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>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isShared && album && assetViewerManager.isShowActivityPanel && $user}
|
{#if isShared && album && assetViewerManager.isShowActivityPanel && $user}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user