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:
Alex 2023-04-08 20:40:37 -05:00 committed by GitHub
parent d76b3c8f78
commit 91e27affeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 25 deletions

24
web/package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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)}

View file

@ -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}

View file

@ -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}