1
0
mirror of https://github.com/immich-app/immich.git synced 2025-06-23 04:38:12 +02:00

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:
Alex
2022-09-09 15:55:20 -05:00
committed by GitHub
parent b6d025da09
commit d856b35afc
9 changed files with 194 additions and 69 deletions

View File

@ -136,7 +136,7 @@
<div
style:width={`${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'
}`}
on:mouseenter={handleMouseOverThumbnail}

View File

@ -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">
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';
export let scrollTop = 0;
// export let viewportWidth = 0;
export let scrollbarHeight = 0;
let timelineHeight = 0;
$: timelineHeight = $assetGridState.timelineHeight;
$: viewportWidth = $assetGridState.viewportWidth;
$: timelineScrolltop = (scrollbarPosition / scrollbarHeight) * timelineHeight;
let segmentScrollbarLayout: SegmentScrollbarLayout[] = [];
let isHover = false;
let isDragging = false;
let hoveredDate: Date;
let currentMouseYLocation = 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;
}
$: {
// let result: SegmentScrollbarLayout[] = [];
// for (const [i, segment] of assetStoreState.entries()) {
// let segmentLayout = new SegmentScrollbarLayout();
// segmentLayout.count = segmentData.groups[i].count;
// segmentLayout.height =
// segment.assets.length == 0
// ? getSegmentHeight(segmentData.groups[i].count)
// : Math.round((segment.segmentHeight / timelineHeight) * scrollbarHeight);
// segmentLayout.timeGroup = segment.segmentDate;
// result.push(segmentLayout);
// }
// segmentScrollbarLayout = result;
let result: SegmentScrollbarLayout[] = [];
for (const bucket of $assetGridState.buckets) {
let segmentLayout = new SegmentScrollbarLayout();
segmentLayout.count = bucket.assets.length;
segmentLayout.height = (bucket.bucketHeight / timelineHeight) * scrollbarHeight;
segmentLayout.timeGroup = bucket.bucketDate;
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) => {
currentMouseYLocation = e.clientY - 71 - 30;
currentMouseYLocation = e.clientY - offset - 30;
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>
<div
id="immich-scubbable-scrollbar"
class="fixed right-0 w-[60px] h-full bg-immich-bg z-[9999] hover:cursor-row-resize"
id="immich-scrubbable-scrollbar"
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:mouseleave={() => (isHover = false)}
on:mouseleave={() => {
isHover = false;
isDragging = false;
}}
on:mouseup={handleMouseUp}
on:mousemove={handleMouseDrag}
on:mousedown={handleMouseDown}
style:height={scrollbarHeight + 'px'}
>
{#if isHover}
<div
@ -81,29 +118,33 @@
{/if}
<!-- Scroll Position Indicator Line -->
<div
class="absolute right-0 w-10 h-[2px] bg-immich-primary"
style:top={scrollbarPosition + 'px'}
/>
{#if !isDragging}
<div
class="absolute right-0 w-10 h-[2px] bg-immich-primary"
style:top={scrollbarPosition + 'px'}
/>
{/if}
<!-- Time Segment -->
{#each segmentScrollbarLayout as segment, index (segment.timeGroup)}
{@const groupDate = new Date(segment.timeGroup)}
<div
class="relative "
id="time-segment"
class="relative"
style:height={segment.height + 'px'}
aria-label={segment.timeGroup + ' ' + segment.count}
on:mousemove={(e) => handleMouseMove(e, groupDate)}
>
{#if new Date(segmentScrollbarLayout[index - 1]?.timeGroup).getFullYear() !== groupDate.getFullYear()}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 pr-3 z-10 text-xs font-medium"
>
{groupDate.getFullYear()}
</div>
{:else if segment.count > 5}
{#if segment.height > 8}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 pr-5 z-10 text-xs font-medium"
>
{groupDate.getFullYear()}
</div>
{/if}
{:else if segment.height > 5}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 rounded-full h-[4px] w-[4px] mr-3 bg-gray-300 block"
@ -114,7 +155,8 @@
</div>
<style>
#immich-scubbable-scrollbar {
#immich-scrubbable-scrollbar,
#time-segment {
contain: layout;
}
</style>

View File

@ -18,6 +18,7 @@
let showSharingCount = false;
let showAlbumsCount = false;
// let domCount = 0;
onMount(async () => {
if ($page.routeId == 'albums') {
selectedAction = AppSideBarSelection.ALBUMS;
@ -26,6 +27,10 @@
} else if ($page.routeId == 'sharing') {
selectedAction = AppSideBarSelection.SHARING;
}
// setInterval(() => {
// domCount = document.getElementsByTagName('*').length;
// }, 500);
});
const getAssetCount = async () => {
@ -48,6 +53,7 @@
</script>
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
<!-- {domCount} -->
<a
sveltekit:prefetch
sveltekit:noscroll
@ -110,7 +116,7 @@
<LoadingSpinner />
{:then data}
<div>
<p>{data.shared + data.sharing} albums</p>
<p>{data.shared + data.sharing} Albums</p>
</div>
{/await}
</div>
@ -145,7 +151,7 @@
<LoadingSpinner />
{:then data}
<div>
<p>{data.owned} albums</p>
<p>{data.owned} Albums</p>
</div>
{/await}
</div>