feat(web): add justify layout for GalleryViewer (#2207)
* Implemented justify layout * Fixed issue with asset selection does not show style for selected assets * pr feedback * PR feedback * fix test * Added flip animation
This commit is contained in:
parent
d76b3c8f78
commit
91e27affeb
5 changed files with 79 additions and 25 deletions
24
web/package-lock.json
generated
24
web/package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"justified-layout": "^4.1.0",
|
||||
"leaflet": "^1.9.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.2.1",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/justified-layout": "^4.1.0",
|
||||
"@types/leaflet": "^1.9.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/luxon": "^3.2.0",
|
||||
|
@ -3605,6 +3607,12 @@
|
|||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/justified-layout": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||
"integrity": "sha512-D8u3yfJjx1xjRJxWUVJlTmFxSN0ph5BpkQo9dTIO1LpYFu0WS1XcWJoNAKJZql+jyiZB5eHt0UKxC1wsmO3/LQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz",
|
||||
|
@ -9008,6 +9016,11 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/justified-layout": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
|
@ -14027,6 +14040,12 @@
|
|||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/justified-layout": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||
"integrity": "sha512-D8u3yfJjx1xjRJxWUVJlTmFxSN0ph5BpkQo9dTIO1LpYFu0WS1XcWJoNAKJZql+jyiZB5eHt0UKxC1wsmO3/LQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/leaflet": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz",
|
||||
|
@ -18004,6 +18023,11 @@
|
|||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
},
|
||||
"justified-layout": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/justified-layout": "^4.1.0",
|
||||
"@types/leaflet": "^1.9.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/luxon": "^3.2.0",
|
||||
|
@ -58,6 +59,7 @@
|
|||
"axios": "^0.27.2",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"justified-layout": "^4.1.0",
|
||||
"leaflet": "^1.9.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.2.1",
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
export let asset: AssetResponseDto;
|
||||
export let groupIndex = 0;
|
||||
export let thumbnailSize: number | undefined = undefined;
|
||||
export let thumbnailWidth: number | undefined = undefined;
|
||||
export let thumbnailHeight: number | undefined = undefined;
|
||||
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
|
||||
export let selected = false;
|
||||
export let disabled = false;
|
||||
|
@ -30,6 +32,10 @@
|
|||
return [thumbnailSize, thumbnailSize];
|
||||
}
|
||||
|
||||
if (thumbnailWidth && thumbnailHeight) {
|
||||
return [thumbnailWidth, thumbnailHeight];
|
||||
}
|
||||
|
||||
if (asset.exifInfo?.orientation === 'Rotate 90 CW') {
|
||||
return [176, 235];
|
||||
} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') {
|
||||
|
@ -57,7 +63,9 @@
|
|||
<div
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
class="relative group {disabled ? 'bg-gray-300' : 'bg-immich-primary/20'}"
|
||||
class="relative group {disabled
|
||||
? 'bg-gray-300'
|
||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:hover:cursor-pointer={!disabled}
|
||||
on:mouseenter={() => (mouseOver = true)}
|
||||
|
|
|
@ -162,7 +162,8 @@
|
|||
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
|
||||
selected={$selectedAssets.has(asset)}
|
||||
selected={$selectedAssets.has(asset) ||
|
||||
$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
/>
|
||||
{/each}
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, SharedLinkResponseDto, ThumbnailFormat } from '@api';
|
||||
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
@ -17,20 +18,27 @@
|
|||
let currentViewAssetIndex = 0;
|
||||
|
||||
let viewWidth: number;
|
||||
let thumbnailSize = 300;
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
$: {
|
||||
if (assets.length < 6) {
|
||||
thumbnailSize = Math.min(320, Math.floor(viewWidth / assets.length - assets.length));
|
||||
} else {
|
||||
if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
||||
else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
|
||||
else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
|
||||
else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
|
||||
else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
|
||||
function getAssetRatio(asset: AssetResponseDto): number {
|
||||
const height = asset.exifInfo?.exifImageHeight;
|
||||
const width = asset.exifInfo?.exifImageWidth;
|
||||
const orientation = Number(asset.exifInfo?.orientation);
|
||||
|
||||
if (height && width) {
|
||||
if (orientation) {
|
||||
if (orientation == 6 || orientation == -90) {
|
||||
return height / width;
|
||||
} else {
|
||||
return width / height;
|
||||
}
|
||||
}
|
||||
|
||||
return width / height;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const viewAssetHandler = (event: CustomEvent) => {
|
||||
|
@ -93,18 +101,29 @@
|
|||
|
||||
{#if assets.length > 0}
|
||||
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
||||
{#each assets as asset (asset.id)}
|
||||
<Thumbnail
|
||||
{asset}
|
||||
{thumbnailSize}
|
||||
readonly={disableAssetSelect}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
selected={selectedAssets.has(asset)}
|
||||
/>
|
||||
{/each}
|
||||
{#if viewWidth}
|
||||
{@const geoArray = assets.map(getAssetRatio)}
|
||||
{@const justifiedLayoutResult = justifiedLayout(geoArray, {
|
||||
targetRowHeight: 235,
|
||||
containerWidth: Math.floor(viewWidth)
|
||||
})}
|
||||
|
||||
{#each assets as asset, index (asset.id)}
|
||||
<div animate:flip={{ duration: 500 }}>
|
||||
<Thumbnail
|
||||
{asset}
|
||||
thumbnailWidth={justifiedLayoutResult.boxes[index].width || 235}
|
||||
thumbnailHeight={justifiedLayoutResult.boxes[index].height || 235}
|
||||
readonly={disableAssetSelect}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
selected={selectedAssets.has(asset)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
Loading…
Reference in a new issue