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