forked from Cutlery/immich
		
	feat(web) add scrollbar with timeline information (#658)
- Implement a scrollbar with a timeline similar to Google Photos - The scrollbar can also be dragged
This commit is contained in:
		
							parent
							
								
									b6d025da09
								
							
						
					
					
						commit
						d856b35afc
					
				@ -88,4 +88,11 @@ input:focus-visible {
 | 
				
			|||||||
		background: #4250afad;
 | 
							background: #4250afad;
 | 
				
			||||||
		border-radius: 16px;
 | 
							border-radius: 16px;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* Hidden scrollbar */
 | 
				
			||||||
 | 
						/* width */
 | 
				
			||||||
 | 
						.scrollbar-hidden::-webkit-scrollbar {
 | 
				
			||||||
 | 
							display: none;
 | 
				
			||||||
 | 
							scrollbar-width: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -26,12 +26,16 @@
 | 
				
			|||||||
		NotificationType
 | 
							NotificationType
 | 
				
			||||||
	} from '../shared-components/notification/notification';
 | 
						} from '../shared-components/notification/notification';
 | 
				
			||||||
	import { browser } from '$app/env';
 | 
						import { browser } from '$app/env';
 | 
				
			||||||
 | 
						import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let album: AlbumResponseDto;
 | 
						export let album: AlbumResponseDto;
 | 
				
			||||||
 | 
						const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let isShowAssetViewer = false;
 | 
						let isShowAssetViewer = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let isShowAssetSelection = false;
 | 
						let isShowAssetSelection = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: $isAlbumAssetSelectionOpen = isShowAssetSelection;
 | 
				
			||||||
	$: {
 | 
						$: {
 | 
				
			||||||
		if (browser) {
 | 
							if (browser) {
 | 
				
			||||||
			if (isShowAssetSelection) {
 | 
								if (isShowAssetSelection) {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
 | 
						import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
 | 
				
			||||||
	import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
 | 
						import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
 | 
				
			||||||
	import { api, TimeGroupEnum } from '@api';
 | 
						import { api, AssetCountByTimeBucketResponseDto, TimeGroupEnum } from '@api';
 | 
				
			||||||
	import AssetDateGroup from './asset-date-group.svelte';
 | 
						import AssetDateGroup from './asset-date-group.svelte';
 | 
				
			||||||
	import Portal from '../shared-components/portal/portal.svelte';
 | 
						import Portal from '../shared-components/portal/portal.svelte';
 | 
				
			||||||
	import AssetViewer from '../asset-viewer/asset-viewer.svelte';
 | 
						import AssetViewer from '../asset-viewer/asset-viewer.svelte';
 | 
				
			||||||
@ -12,16 +12,23 @@
 | 
				
			|||||||
		isViewingAssetStoreState,
 | 
							isViewingAssetStoreState,
 | 
				
			||||||
		viewingAssetStoreState
 | 
							viewingAssetStoreState
 | 
				
			||||||
	} from '$lib/stores/asset-interaction.store';
 | 
						} from '$lib/stores/asset-interaction.store';
 | 
				
			||||||
 | 
						import Scrollbar, {
 | 
				
			||||||
 | 
							OnScrollbarClickDetail,
 | 
				
			||||||
 | 
							OnScrollbarDragDetail
 | 
				
			||||||
 | 
						} from '../shared-components/scrollbar/scrollbar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let isAlbumSelectionMode = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let viewportHeight = 0;
 | 
						let viewportHeight = 0;
 | 
				
			||||||
	let viewportWidth = 0;
 | 
						let viewportWidth = 0;
 | 
				
			||||||
	let assetGridElement: HTMLElement;
 | 
						let assetGridElement: HTMLElement;
 | 
				
			||||||
	export let isAlbumSelectionMode = false;
 | 
						let bucketInfo: AssetCountByTimeBucketResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onMount(async () => {
 | 
						onMount(async () => {
 | 
				
			||||||
		const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({
 | 
							const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({
 | 
				
			||||||
			timeGroup: TimeGroupEnum.Month
 | 
								timeGroup: TimeGroupEnum.Month
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
							bucketInfo = assetCountByTimebucket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket);
 | 
							assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -60,14 +67,46 @@
 | 
				
			|||||||
	const navigateToNextAsset = () => {
 | 
						const navigateToNextAsset = () => {
 | 
				
			||||||
		assetInteractionStore.navigateAsset('next');
 | 
							assetInteractionStore.navigateAsset('next');
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let lastScrollPosition = 0;
 | 
				
			||||||
 | 
						let animationTick = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleTimelineScroll = () => {
 | 
				
			||||||
 | 
							if (!animationTick) {
 | 
				
			||||||
 | 
								window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
									lastScrollPosition = assetGridElement?.scrollTop;
 | 
				
			||||||
 | 
									animationTick = false;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								animationTick = true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleScrollbarClick = (e: OnScrollbarClickDetail) => {
 | 
				
			||||||
 | 
							assetGridElement.scrollTop = e.scrollTo;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
 | 
				
			||||||
 | 
							assetGridElement.scrollTop = e.scrollTo;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
 | 
				
			||||||
 | 
						<Scrollbar
 | 
				
			||||||
 | 
							scrollbarHeight={viewportHeight}
 | 
				
			||||||
 | 
							scrollTop={lastScrollPosition}
 | 
				
			||||||
 | 
							on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
 | 
				
			||||||
 | 
							on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section
 | 
					<section
 | 
				
			||||||
	id="asset-grid"
 | 
						id="asset-grid"
 | 
				
			||||||
	class="overflow-y-auto pl-4"
 | 
						class="overflow-y-auto pl-4 scrollbar-hidden"
 | 
				
			||||||
	bind:clientHeight={viewportHeight}
 | 
						bind:clientHeight={viewportHeight}
 | 
				
			||||||
	bind:clientWidth={viewportWidth}
 | 
						bind:clientWidth={viewportWidth}
 | 
				
			||||||
	bind:this={assetGridElement}
 | 
						bind:this={assetGridElement}
 | 
				
			||||||
 | 
						on:scroll={handleTimelineScroll}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	{#if assetGridElement}
 | 
						{#if assetGridElement}
 | 
				
			||||||
		<section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
 | 
							<section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
 | 
				
			||||||
@ -117,5 +156,6 @@
 | 
				
			|||||||
<style>
 | 
					<style>
 | 
				
			||||||
	#asset-grid {
 | 
						#asset-grid {
 | 
				
			||||||
		contain: layout;
 | 
							contain: layout;
 | 
				
			||||||
 | 
							scrollbar-width: none;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -136,7 +136,7 @@
 | 
				
			|||||||
	<div
 | 
						<div
 | 
				
			||||||
		style:width={`${thumbnailSize}px`}
 | 
							style:width={`${thumbnailSize}px`}
 | 
				
			||||||
		style:height={`${thumbnailSize}px`}
 | 
							style:height={`${thumbnailSize}px`}
 | 
				
			||||||
		class={`bg-gray-100 relative  ${getSize()} ${
 | 
							class={`bg-gray-100 relative select-none  ${getSize()} ${
 | 
				
			||||||
			disabled ? 'cursor-not-allowed' : 'hover:cursor-pointer'
 | 
								disabled ? 'cursor-not-allowed' : 'hover:cursor-pointer'
 | 
				
			||||||
		}`}
 | 
							}`}
 | 
				
			||||||
		on:mouseenter={handleMouseOverThumbnail}
 | 
							on:mouseenter={handleMouseOverThumbnail}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,74 +1,111 @@
 | 
				
			|||||||
 | 
					<script lang="ts" context="module">
 | 
				
			||||||
 | 
						type OnScrollbarClick = {
 | 
				
			||||||
 | 
							onscrollbarclick: OnScrollbarClickDetail;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export type OnScrollbarClickDetail = {
 | 
				
			||||||
 | 
							scrollTo: number;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type OnScrollbarDrag = {
 | 
				
			||||||
 | 
							onscrollbardrag: OnScrollbarDragDetail;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export type OnScrollbarDragDetail = {
 | 
				
			||||||
 | 
							scrollTo: number;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { onMount } from 'svelte';
 | 
						import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { assetGridState } from '$lib/stores/assets.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
	import { SegmentScrollbarLayout } from './segment-scrollbar-layout';
 | 
						import { SegmentScrollbarLayout } from './segment-scrollbar-layout';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let scrollTop = 0;
 | 
						export let scrollTop = 0;
 | 
				
			||||||
	// export let viewportWidth = 0;
 | 
					 | 
				
			||||||
	export let scrollbarHeight = 0;
 | 
						export let scrollbarHeight = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let timelineHeight = 0;
 | 
						$: timelineHeight = $assetGridState.timelineHeight;
 | 
				
			||||||
 | 
						$: viewportWidth = $assetGridState.viewportWidth;
 | 
				
			||||||
 | 
						$: timelineScrolltop = (scrollbarPosition / scrollbarHeight) * timelineHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let segmentScrollbarLayout: SegmentScrollbarLayout[] = [];
 | 
						let segmentScrollbarLayout: SegmentScrollbarLayout[] = [];
 | 
				
			||||||
	let isHover = false;
 | 
						let isHover = false;
 | 
				
			||||||
 | 
						let isDragging = false;
 | 
				
			||||||
	let hoveredDate: Date;
 | 
						let hoveredDate: Date;
 | 
				
			||||||
	let currentMouseYLocation = 0;
 | 
						let currentMouseYLocation = 0;
 | 
				
			||||||
	let scrollbarPosition = 0;
 | 
						let scrollbarPosition = 0;
 | 
				
			||||||
 | 
						let animationTick = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
 | 
				
			||||||
 | 
						$: offset = $isAlbumAssetSelectionOpen ? 100 : 71;
 | 
				
			||||||
 | 
						const dispatchClick = createEventDispatcher<OnScrollbarClick>();
 | 
				
			||||||
 | 
						const dispatchDrag = createEventDispatcher<OnScrollbarDrag>();
 | 
				
			||||||
	$: {
 | 
						$: {
 | 
				
			||||||
		scrollbarPosition = (scrollTop / timelineHeight) * scrollbarHeight;
 | 
							scrollbarPosition = (scrollTop / timelineHeight) * scrollbarHeight;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: {
 | 
						$: {
 | 
				
			||||||
		// let result: SegmentScrollbarLayout[] = [];
 | 
							let result: SegmentScrollbarLayout[] = [];
 | 
				
			||||||
		// for (const [i, segment] of assetStoreState.entries()) {
 | 
							for (const bucket of $assetGridState.buckets) {
 | 
				
			||||||
		// 	let segmentLayout = new SegmentScrollbarLayout();
 | 
								let segmentLayout = new SegmentScrollbarLayout();
 | 
				
			||||||
		// 	segmentLayout.count = segmentData.groups[i].count;
 | 
								segmentLayout.count = bucket.assets.length;
 | 
				
			||||||
		// 	segmentLayout.height =
 | 
								segmentLayout.height = (bucket.bucketHeight / timelineHeight) * scrollbarHeight;
 | 
				
			||||||
		// 		segment.assets.length == 0
 | 
								segmentLayout.timeGroup = bucket.bucketDate;
 | 
				
			||||||
		// 			? getSegmentHeight(segmentData.groups[i].count)
 | 
								result.push(segmentLayout);
 | 
				
			||||||
		// 			: Math.round((segment.segmentHeight / timelineHeight) * scrollbarHeight);
 | 
							}
 | 
				
			||||||
		// 	segmentLayout.timeGroup = segment.segmentDate;
 | 
							segmentScrollbarLayout = result;
 | 
				
			||||||
		// 	result.push(segmentLayout);
 | 
					 | 
				
			||||||
		// }
 | 
					 | 
				
			||||||
		// segmentScrollbarLayout = result;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onMount(() => {
 | 
					 | 
				
			||||||
		// segmentScrollbarLayout = getLayoutDistance();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// const getSegmentHeight = (groupCount: number) => {
 | 
					 | 
				
			||||||
	// if (segmentData.groups.length > 0) {
 | 
					 | 
				
			||||||
	// 	const percentage = (groupCount * 100) / segmentData.totalAssets;
 | 
					 | 
				
			||||||
	// 	return Math.round((percentage * scrollbarHeight) / 100);
 | 
					 | 
				
			||||||
	// } else {
 | 
					 | 
				
			||||||
	// 	return 0;
 | 
					 | 
				
			||||||
	// }
 | 
					 | 
				
			||||||
	// };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// const getLayoutDistance = () => {
 | 
					 | 
				
			||||||
	// let result: SegmentScrollbarLayout[] = [];
 | 
					 | 
				
			||||||
	// for (const segment of segmentData.groups) {
 | 
					 | 
				
			||||||
	// 	let segmentLayout = new SegmentScrollbarLayout();
 | 
					 | 
				
			||||||
	// 	segmentLayout.count = segment.count;
 | 
					 | 
				
			||||||
	// 	segmentLayout.height = getSegmentHeight(segment.count);
 | 
					 | 
				
			||||||
	// 	segmentLayout.timeGroup = segment.timeGroup;
 | 
					 | 
				
			||||||
	// 	result.push(segmentLayout);
 | 
					 | 
				
			||||||
	// }
 | 
					 | 
				
			||||||
	// return result;
 | 
					 | 
				
			||||||
	// };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const handleMouseMove = (e: MouseEvent, currentDate: Date) => {
 | 
						const handleMouseMove = (e: MouseEvent, currentDate: Date) => {
 | 
				
			||||||
		currentMouseYLocation = e.clientY - 71 - 30;
 | 
							currentMouseYLocation = e.clientY - offset - 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		hoveredDate = new Date(currentDate.toISOString().slice(0, -1));
 | 
							hoveredDate = new Date(currentDate.toISOString().slice(0, -1));
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleMouseDown = (e: MouseEvent) => {
 | 
				
			||||||
 | 
							isDragging = true;
 | 
				
			||||||
 | 
							scrollbarPosition = e.clientY - offset;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleMouseUp = (e: MouseEvent) => {
 | 
				
			||||||
 | 
							isDragging = false;
 | 
				
			||||||
 | 
							scrollbarPosition = e.clientY - offset;
 | 
				
			||||||
 | 
							dispatchClick('onscrollbarclick', { scrollTo: timelineScrolltop });
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleMouseDrag = (e: MouseEvent) => {
 | 
				
			||||||
 | 
							if (isDragging) {
 | 
				
			||||||
 | 
								if (!animationTick) {
 | 
				
			||||||
 | 
									window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
										const dy = e.clientY - scrollbarPosition - offset;
 | 
				
			||||||
 | 
										scrollbarPosition += dy;
 | 
				
			||||||
 | 
										dispatchDrag('onscrollbardrag', { scrollTo: timelineScrolltop });
 | 
				
			||||||
 | 
										animationTick = false;
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									animationTick = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
	id="immich-scubbable-scrollbar"
 | 
						id="immich-scrubbable-scrollbar"
 | 
				
			||||||
	class="fixed right-0 w-[60px] h-full bg-immich-bg z-[9999] hover:cursor-row-resize"
 | 
						class="fixed right-0 bg-immich-bg z-10 hover:cursor-row-resize select-none"
 | 
				
			||||||
 | 
						style:width={isDragging ? '100vw' : '60px'}
 | 
				
			||||||
 | 
						style:background-color={isDragging ? 'transparent' : 'transparent'}
 | 
				
			||||||
	on:mouseenter={() => (isHover = true)}
 | 
						on:mouseenter={() => (isHover = true)}
 | 
				
			||||||
	on:mouseleave={() => (isHover = false)}
 | 
						on:mouseleave={() => {
 | 
				
			||||||
 | 
							isHover = false;
 | 
				
			||||||
 | 
							isDragging = false;
 | 
				
			||||||
 | 
						}}
 | 
				
			||||||
 | 
						on:mouseup={handleMouseUp}
 | 
				
			||||||
 | 
						on:mousemove={handleMouseDrag}
 | 
				
			||||||
 | 
						on:mousedown={handleMouseDown}
 | 
				
			||||||
 | 
						style:height={scrollbarHeight + 'px'}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	{#if isHover}
 | 
						{#if isHover}
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
@ -81,29 +118,33 @@
 | 
				
			|||||||
	{/if}
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<!-- Scroll Position Indicator Line -->
 | 
						<!-- Scroll Position Indicator Line -->
 | 
				
			||||||
 | 
						{#if !isDragging}
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
			class="absolute right-0 w-10 h-[2px] bg-immich-primary"
 | 
								class="absolute right-0 w-10 h-[2px] bg-immich-primary"
 | 
				
			||||||
			style:top={scrollbarPosition + 'px'}
 | 
								style:top={scrollbarPosition + 'px'}
 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
	<!-- Time Segment -->
 | 
						<!-- Time Segment -->
 | 
				
			||||||
	{#each segmentScrollbarLayout as segment, index (segment.timeGroup)}
 | 
						{#each segmentScrollbarLayout as segment, index (segment.timeGroup)}
 | 
				
			||||||
		{@const groupDate = new Date(segment.timeGroup)}
 | 
							{@const groupDate = new Date(segment.timeGroup)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<div
 | 
							<div
 | 
				
			||||||
			class="relative "
 | 
								id="time-segment"
 | 
				
			||||||
 | 
								class="relative"
 | 
				
			||||||
			style:height={segment.height + 'px'}
 | 
								style:height={segment.height + 'px'}
 | 
				
			||||||
			aria-label={segment.timeGroup + ' ' + segment.count}
 | 
								aria-label={segment.timeGroup + ' ' + segment.count}
 | 
				
			||||||
			on:mousemove={(e) => handleMouseMove(e, groupDate)}
 | 
								on:mousemove={(e) => handleMouseMove(e, groupDate)}
 | 
				
			||||||
		>
 | 
							>
 | 
				
			||||||
			{#if new Date(segmentScrollbarLayout[index - 1]?.timeGroup).getFullYear() !== groupDate.getFullYear()}
 | 
								{#if new Date(segmentScrollbarLayout[index - 1]?.timeGroup).getFullYear() !== groupDate.getFullYear()}
 | 
				
			||||||
 | 
									{#if segment.height > 8}
 | 
				
			||||||
					<div
 | 
										<div
 | 
				
			||||||
						aria-label={segment.timeGroup + ' ' + segment.count}
 | 
											aria-label={segment.timeGroup + ' ' + segment.count}
 | 
				
			||||||
					class="absolute right-0 pr-3 z-10 text-xs font-medium"
 | 
											class="absolute right-0 pr-5 z-10 text-xs font-medium"
 | 
				
			||||||
					>
 | 
										>
 | 
				
			||||||
						{groupDate.getFullYear()}
 | 
											{groupDate.getFullYear()}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
			{:else if segment.count > 5}
 | 
									{/if}
 | 
				
			||||||
 | 
								{:else if segment.height > 5}
 | 
				
			||||||
				<div
 | 
									<div
 | 
				
			||||||
					aria-label={segment.timeGroup + ' ' + segment.count}
 | 
										aria-label={segment.timeGroup + ' ' + segment.count}
 | 
				
			||||||
					class="absolute right-0 rounded-full h-[4px] w-[4px] mr-3 bg-gray-300 block"
 | 
										class="absolute right-0 rounded-full h-[4px] w-[4px] mr-3 bg-gray-300 block"
 | 
				
			||||||
@ -114,7 +155,8 @@
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
	#immich-scubbable-scrollbar {
 | 
						#immich-scrubbable-scrollbar,
 | 
				
			||||||
 | 
						#time-segment {
 | 
				
			||||||
		contain: layout;
 | 
							contain: layout;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@
 | 
				
			|||||||
	let showSharingCount = false;
 | 
						let showSharingCount = false;
 | 
				
			||||||
	let showAlbumsCount = false;
 | 
						let showAlbumsCount = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// let domCount = 0;
 | 
				
			||||||
	onMount(async () => {
 | 
						onMount(async () => {
 | 
				
			||||||
		if ($page.routeId == 'albums') {
 | 
							if ($page.routeId == 'albums') {
 | 
				
			||||||
			selectedAction = AppSideBarSelection.ALBUMS;
 | 
								selectedAction = AppSideBarSelection.ALBUMS;
 | 
				
			||||||
@ -26,6 +27,10 @@
 | 
				
			|||||||
		} else if ($page.routeId == 'sharing') {
 | 
							} else if ($page.routeId == 'sharing') {
 | 
				
			||||||
			selectedAction = AppSideBarSelection.SHARING;
 | 
								selectedAction = AppSideBarSelection.SHARING;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// setInterval(() => {
 | 
				
			||||||
 | 
							// 	domCount = document.getElementsByTagName('*').length;
 | 
				
			||||||
 | 
							// }, 500);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const getAssetCount = async () => {
 | 
						const getAssetCount = async () => {
 | 
				
			||||||
@ -48,6 +53,7 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
 | 
					<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
 | 
				
			||||||
 | 
						<!-- {domCount} -->
 | 
				
			||||||
	<a
 | 
						<a
 | 
				
			||||||
		sveltekit:prefetch
 | 
							sveltekit:prefetch
 | 
				
			||||||
		sveltekit:noscroll
 | 
							sveltekit:noscroll
 | 
				
			||||||
@ -110,7 +116,7 @@
 | 
				
			|||||||
						<LoadingSpinner />
 | 
											<LoadingSpinner />
 | 
				
			||||||
					{:then data}
 | 
										{:then data}
 | 
				
			||||||
						<div>
 | 
											<div>
 | 
				
			||||||
							<p>{data.shared + data.sharing} albums</p>
 | 
												<p>{data.shared + data.sharing} Albums</p>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{/await}
 | 
										{/await}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
@ -145,7 +151,7 @@
 | 
				
			|||||||
						<LoadingSpinner />
 | 
											<LoadingSpinner />
 | 
				
			||||||
					{:then data}
 | 
										{:then data}
 | 
				
			||||||
						<div>
 | 
											<div>
 | 
				
			||||||
							<p>{data.owned} albums</p>
 | 
												<p>{data.owned} Albums</p>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{/await}
 | 
										{/await}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								web/src/lib/stores/album-asset-selection.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/src/lib/stores/album-asset-selection.store.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createAlbumAssetSelectionStore() {
 | 
				
			||||||
 | 
						const isAlbumAssetSelectionOpen = writable<boolean>(false);
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							isAlbumAssetSelectionOpen
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const albumAssetSelectionStore = createAlbumAssetSelectionStore();
 | 
				
			||||||
@ -34,7 +34,7 @@ function createAssetStore() {
 | 
				
			|||||||
		assetGridState.set({
 | 
							assetGridState.set({
 | 
				
			||||||
			viewportHeight,
 | 
								viewportHeight,
 | 
				
			||||||
			viewportWidth,
 | 
								viewportWidth,
 | 
				
			||||||
			timelineHeight: calculateViewportHeightByNumberOfAsset(data.totalCount, viewportWidth),
 | 
								timelineHeight: 0,
 | 
				
			||||||
			buckets: data.buckets.map((d) => ({
 | 
								buckets: data.buckets.map((d) => ({
 | 
				
			||||||
				bucketDate: d.timeBucket,
 | 
									bucketDate: d.timeBucket,
 | 
				
			||||||
				bucketHeight: calculateViewportHeightByNumberOfAsset(d.count, viewportWidth),
 | 
									bucketHeight: calculateViewportHeightByNumberOfAsset(d.count, viewportWidth),
 | 
				
			||||||
@ -43,6 +43,12 @@ function createAssetStore() {
 | 
				
			|||||||
			})),
 | 
								})),
 | 
				
			||||||
			assets: []
 | 
								assets: []
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Update timeline height based on calculated bucket height
 | 
				
			||||||
 | 
							assetGridState.update((state) => {
 | 
				
			||||||
 | 
								state.timelineHeight = lodash.sumBy(state.buckets, (d) => d.bucketHeight);
 | 
				
			||||||
 | 
								return state;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const getAssetsByBucket = async (bucket: string) => {
 | 
						const getAssetsByBucket = async (bucket: string) => {
 | 
				
			||||||
@ -108,10 +114,19 @@ function createAssetStore() {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const updateBucketHeight = (bucket: string, height: number) => {
 | 
						const updateBucketHeight = (bucket: string, actualBucketHeight: number) => {
 | 
				
			||||||
		assetGridState.update((state) => {
 | 
							assetGridState.update((state) => {
 | 
				
			||||||
			const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
 | 
								const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
 | 
				
			||||||
			state.buckets[bucketIndex].bucketHeight = height;
 | 
								// Update timeline height based on the new bucket height
 | 
				
			||||||
 | 
								const estimateBucketHeight = state.buckets[bucketIndex].bucketHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (actualBucketHeight >= estimateBucketHeight) {
 | 
				
			||||||
 | 
									state.timelineHeight += actualBucketHeight - estimateBucketHeight;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									state.timelineHeight -= estimateBucketHeight - actualBucketHeight;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								state.buckets[bucketIndex].bucketHeight = actualBucketHeight;
 | 
				
			||||||
			return state;
 | 
								return state;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
				
			|||||||
@ -4,9 +4,10 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function calculateViewportHeightByNumberOfAsset(assetCount: number, viewportWidth: number) {
 | 
					export function calculateViewportHeightByNumberOfAsset(assetCount: number, viewportWidth: number) {
 | 
				
			||||||
	const thumbnailHeight = 235;
 | 
						const thumbnailHeight = 237;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const unwrappedWidth = (3 / 2) * assetCount * thumbnailHeight * (7 / 10);
 | 
						// const unwrappedWidth = (3 / 2) * assetCount * thumbnailHeight * (7 / 10);
 | 
				
			||||||
 | 
						const unwrappedWidth = assetCount * thumbnailHeight;
 | 
				
			||||||
	const rows = Math.ceil(unwrappedWidth / viewportWidth);
 | 
						const rows = Math.ceil(unwrappedWidth / viewportWidth);
 | 
				
			||||||
	const height = rows * thumbnailHeight;
 | 
						const height = rows * thumbnailHeight;
 | 
				
			||||||
	return height;
 | 
						return height;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user