mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										24
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										24
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -11,6 +11,7 @@ | |||||||
| 				"axios": "^0.27.2", | 				"axios": "^0.27.2", | ||||||
| 				"copy-image-clipboard": "^2.1.2", | 				"copy-image-clipboard": "^2.1.2", | ||||||
| 				"handlebars": "^4.7.7", | 				"handlebars": "^4.7.7", | ||||||
|  | 				"justified-layout": "^4.1.0", | ||||||
| 				"leaflet": "^1.9.3", | 				"leaflet": "^1.9.3", | ||||||
| 				"lodash-es": "^4.17.21", | 				"lodash-es": "^4.17.21", | ||||||
| 				"luxon": "^3.2.1", | 				"luxon": "^3.2.1", | ||||||
| @@ -28,6 +29,7 @@ | |||||||
| 				"@testing-library/jest-dom": "^5.16.5", | 				"@testing-library/jest-dom": "^5.16.5", | ||||||
| 				"@testing-library/svelte": "^3.2.2", | 				"@testing-library/svelte": "^3.2.2", | ||||||
| 				"@types/cookie": "^0.5.1", | 				"@types/cookie": "^0.5.1", | ||||||
|  | 				"@types/justified-layout": "^4.1.0", | ||||||
| 				"@types/leaflet": "^1.9.1", | 				"@types/leaflet": "^1.9.1", | ||||||
| 				"@types/lodash-es": "^4.17.6", | 				"@types/lodash-es": "^4.17.6", | ||||||
| 				"@types/luxon": "^3.2.0", | 				"@types/luxon": "^3.2.0", | ||||||
| @@ -3605,6 +3607,12 @@ | |||||||
| 			"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", | 			"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", | ||||||
| 			"dev": true | 			"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": { | 		"node_modules/@types/leaflet": { | ||||||
| 			"version": "1.9.1", | 			"version": "1.9.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz", | 			"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz", | ||||||
| @@ -9008,6 +9016,11 @@ | |||||||
| 				"node": ">=6" | 				"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": { | 		"node_modules/kind-of": { | ||||||
| 			"version": "6.0.3", | 			"version": "6.0.3", | ||||||
| 			"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | 			"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==", | 			"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", | ||||||
| 			"dev": true | 			"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": { | 		"@types/leaflet": { | ||||||
| 			"version": "1.9.1", | 			"version": "1.9.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz", | 			"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.1.tgz", | ||||||
| @@ -18004,6 +18023,11 @@ | |||||||
| 			"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", | 			"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", | ||||||
| 			"dev": true | 			"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": { | 		"kind-of": { | ||||||
| 			"version": "6.0.3", | 			"version": "6.0.3", | ||||||
| 			"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | 			"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/jest-dom": "^5.16.5", | ||||||
| 		"@testing-library/svelte": "^3.2.2", | 		"@testing-library/svelte": "^3.2.2", | ||||||
| 		"@types/cookie": "^0.5.1", | 		"@types/cookie": "^0.5.1", | ||||||
|  | 		"@types/justified-layout": "^4.1.0", | ||||||
| 		"@types/leaflet": "^1.9.1", | 		"@types/leaflet": "^1.9.1", | ||||||
| 		"@types/lodash-es": "^4.17.6", | 		"@types/lodash-es": "^4.17.6", | ||||||
| 		"@types/luxon": "^3.2.0", | 		"@types/luxon": "^3.2.0", | ||||||
| @@ -58,6 +59,7 @@ | |||||||
| 		"axios": "^0.27.2", | 		"axios": "^0.27.2", | ||||||
| 		"copy-image-clipboard": "^2.1.2", | 		"copy-image-clipboard": "^2.1.2", | ||||||
| 		"handlebars": "^4.7.7", | 		"handlebars": "^4.7.7", | ||||||
|  | 		"justified-layout": "^4.1.0", | ||||||
| 		"leaflet": "^1.9.3", | 		"leaflet": "^1.9.3", | ||||||
| 		"lodash-es": "^4.17.21", | 		"lodash-es": "^4.17.21", | ||||||
| 		"luxon": "^3.2.1", | 		"luxon": "^3.2.1", | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ | |||||||
| 	export let asset: AssetResponseDto; | 	export let asset: AssetResponseDto; | ||||||
| 	export let groupIndex = 0; | 	export let groupIndex = 0; | ||||||
| 	export let thumbnailSize: number | undefined = undefined; | 	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 format: ThumbnailFormat = ThumbnailFormat.Webp; | ||||||
| 	export let selected = false; | 	export let selected = false; | ||||||
| 	export let disabled = false; | 	export let disabled = false; | ||||||
| @@ -30,6 +32,10 @@ | |||||||
| 			return [thumbnailSize, thumbnailSize]; | 			return [thumbnailSize, thumbnailSize]; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if (thumbnailWidth && thumbnailHeight) { | ||||||
|  | 			return [thumbnailWidth, thumbnailHeight]; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if (asset.exifInfo?.orientation === 'Rotate 90 CW') { | 		if (asset.exifInfo?.orientation === 'Rotate 90 CW') { | ||||||
| 			return [176, 235]; | 			return [176, 235]; | ||||||
| 		} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') { | 		} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') { | ||||||
| @@ -57,7 +63,9 @@ | |||||||
| 	<div | 	<div | ||||||
| 		style:width="{width}px" | 		style:width="{width}px" | ||||||
| 		style:height="{height}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:cursor-not-allowed={disabled} | ||||||
| 		class:hover:cursor-pointer={!disabled} | 		class:hover:cursor-pointer={!disabled} | ||||||
| 		on:mouseenter={() => (mouseOver = true)} | 		on:mouseenter={() => (mouseOver = true)} | ||||||
|   | |||||||
| @@ -162,7 +162,8 @@ | |||||||
| 						on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)} | 						on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)} | ||||||
| 						on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)} | 						on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)} | ||||||
| 						on:mouse-event={() => assetMouseEventHandler(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} | 						disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1} | ||||||
| 					/> | 					/> | ||||||
| 				{/each} | 				{/each} | ||||||
|   | |||||||
| @@ -3,8 +3,9 @@ | |||||||
| 	import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; | 	import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; | ||||||
| 	import { handleError } from '$lib/utils/handle-error'; | 	import { handleError } from '$lib/utils/handle-error'; | ||||||
| 	import { AssetResponseDto, SharedLinkResponseDto, ThumbnailFormat } from '@api'; | 	import { AssetResponseDto, SharedLinkResponseDto, ThumbnailFormat } from '@api'; | ||||||
|  |  | ||||||
| 	import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; | 	import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; | ||||||
|  | 	import justifiedLayout from 'justified-layout'; | ||||||
|  | 	import { flip } from 'svelte/animate'; | ||||||
|  |  | ||||||
| 	export let assets: AssetResponseDto[]; | 	export let assets: AssetResponseDto[]; | ||||||
| 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
| @@ -17,20 +18,27 @@ | |||||||
| 	let currentViewAssetIndex = 0; | 	let currentViewAssetIndex = 0; | ||||||
|  |  | ||||||
| 	let viewWidth: number; | 	let viewWidth: number; | ||||||
| 	let thumbnailSize = 300; |  | ||||||
|  |  | ||||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||||
|  |  | ||||||
| 	$: { | 	function getAssetRatio(asset: AssetResponseDto): number { | ||||||
| 		if (assets.length < 6) { | 		const height = asset.exifInfo?.exifImageHeight; | ||||||
| 			thumbnailSize = Math.min(320, Math.floor(viewWidth / assets.length - assets.length)); | 		const width = asset.exifInfo?.exifImageWidth; | ||||||
| 		} else { | 		const orientation = Number(asset.exifInfo?.orientation); | ||||||
| 			if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6); |  | ||||||
| 			else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6); | 		if (height && width) { | ||||||
| 			else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6); | 			if (orientation) { | ||||||
| 			else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6); | 				if (orientation == 6 || orientation == -90) { | ||||||
| 			else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6); | 					return height / width; | ||||||
|  | 				} else { | ||||||
|  | 					return width / height; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			return width / height; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		return 1; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const viewAssetHandler = (event: CustomEvent) => { | 	const viewAssetHandler = (event: CustomEvent) => { | ||||||
| @@ -93,18 +101,29 @@ | |||||||
|  |  | ||||||
| {#if assets.length > 0} | {#if assets.length > 0} | ||||||
| 	<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> | 	<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> | ||||||
| 		{#each assets as asset (asset.id)} | 		{#if viewWidth} | ||||||
| 			<Thumbnail | 			{@const geoArray = assets.map(getAssetRatio)} | ||||||
| 				{asset} | 			{@const justifiedLayoutResult = justifiedLayout(geoArray, { | ||||||
| 				{thumbnailSize} | 				targetRowHeight: 235, | ||||||
| 				readonly={disableAssetSelect} | 				containerWidth: Math.floor(viewWidth) | ||||||
| 				publicSharedKey={sharedLink?.key} | 			})} | ||||||
| 				format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} |  | ||||||
| 				on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | 			{#each assets as asset, index (asset.id)} | ||||||
| 				on:select={selectAssetHandler} | 				<div animate:flip={{ duration: 500 }}> | ||||||
| 				selected={selectedAssets.has(asset)} | 					<Thumbnail | ||||||
| 			/> | 						{asset} | ||||||
| 		{/each} | 						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> | 	</div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user