1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 02:46:47 +01:00

fix(web): scrollbar (#3536)

This commit is contained in:
Jason Rasmussen 2023-08-03 14:20:41 -04:00 committed by GitHub
parent b44f8d52ee
commit 6da51deb83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 176 deletions

View file

@ -8,10 +8,7 @@
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
import Portal from '../shared-components/portal/portal.svelte';
import Scrollbar, {
OnScrollbarClickDetail,
OnScrollbarDragDetail,
} from '../shared-components/scrollbar/scrollbar.svelte';
import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte';
import AssetDateGroup from './asset-date-group.svelte';
import MemoryLane from './memory-lane.svelte';
@ -30,13 +27,13 @@
export let assetInteractionStore: AssetInteractionStore;
const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore;
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
const viewport: Viewport = { width: 0, height: 0 };
let assetGridElement: HTMLElement;
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
let element: HTMLElement;
let showShortcuts = false;
$: timelineY = element?.scrollTop || 0;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(async () => {
@ -84,7 +81,7 @@
}
function handleScrollTimeline(event: CustomEvent) {
assetGridElement.scrollBy(0, event.detail.heightDelta);
element.scrollBy(0, event.detail.heightDelta);
}
const navigateToPreviousAsset = async () => {
@ -101,26 +98,18 @@
}
};
let lastScrollPosition = 0;
let animationTick = false;
const handleTimelineScroll = () => {
if (!animationTick) {
window.requestAnimationFrame(() => {
lastScrollPosition = assetGridElement?.scrollTop;
animationTick = false;
});
animationTick = true;
if (animationTick) {
return;
}
};
const handleScrollbarClick = (e: OnScrollbarClickDetail) => {
assetGridElement.scrollTop = e.scrollTo;
};
const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
assetGridElement.scrollTop = e.scrollTo;
animationTick = true;
window.requestAnimationFrame(() => {
timelineY = element?.scrollTop || 0;
animationTick = false;
});
};
const handleArchiveSuccess = (e: CustomEvent) => {
@ -278,26 +267,23 @@
<ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} />
{/if}
{#if $assetStore.timelineHeight > viewport.height}
<Scrollbar
{assetStore}
scrollbarHeight={viewport.height}
scrollTop={lastScrollPosition}
on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
/>
{/if}
<Scrollbar
{assetStore}
height={viewport.height}
{timelineY}
on:scrollTimeline={({ detail }) => (element.scrollTop = detail)}
/>
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
<section
id="asset-grid"
class="scrollbar-hidden mb-4 ml-4 mr-[60px] overflow-y-auto"
class="scrollbar-hidden ml-4 mr-[60px] overflow-y-auto pb-4"
bind:clientHeight={viewport.height}
bind:clientWidth={viewport.width}
bind:this={assetGridElement}
bind:this={element}
on:scroll={handleTimelineScroll}
>
{#if assetGridElement}
{#if element}
{#if showMemoryLane}
<MemoryLane />
{/if}
@ -309,7 +295,7 @@
let:intersecting
top={750}
bottom={750}
root={assetGridElement}
root={element}
>
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
{#if intersecting}

View file

@ -1,158 +1,128 @@
<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 { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
import { createEventDispatcher } from 'svelte';
import { SegmentScrollbarLayout } from './segment-scrollbar-layout';
import type { AssetStore } from '$lib/stores/assets.store';
import { createEventDispatcher } from 'svelte';
export let scrollTop = 0;
export let scrollbarHeight = 0;
export let timelineY = 0;
export let height = 0;
export let assetStore: AssetStore;
$: timelineHeight = $assetStore.timelineHeight;
$: 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;
let isAnimating = false;
let hoverLabel = '';
let clientY = 0;
let windowHeight = 0;
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
$: offset = $isAlbumAssetSelectionOpen ? 100 : 76;
const dispatchClick = createEventDispatcher<OnScrollbarClick>();
const dispatchDrag = createEventDispatcher<OnScrollbarDrag>();
$: {
scrollbarPosition = (scrollTop / timelineHeight) * scrollbarHeight;
}
const toScrollY = (timelineY: number) => (timelineY / $assetStore.timelineHeight) * height;
const toTimelineY = (scrollY: number) => Math.round((scrollY * $assetStore.timelineHeight) / height);
$: {
let result: SegmentScrollbarLayout[] = [];
for (const bucket of $assetStore.buckets) {
let segmentLayout = new SegmentScrollbarLayout();
segmentLayout.count = bucket.assets.length;
segmentLayout.height = (bucket.bucketHeight / timelineHeight) * scrollbarHeight;
segmentLayout.timeGroup = bucket.bucketDate;
result.push(segmentLayout);
const HOVER_DATE_HEIGHT = 30;
$: hoverY = height - windowHeight + clientY;
$: scrollY = toScrollY(timelineY);
$: segments = $assetStore.buckets.map((bucket) => ({
count: bucket.assets.length,
height: toScrollY(bucket.bucketHeight),
timeGroup: bucket.bucketDate,
}));
const dispatch = createEventDispatcher<{ scrollTimeline: number }>();
const scrollTimeline = () => dispatch('scrollTimeline', toTimelineY(hoverY));
const handleMouseEvent = (event: { clientY: number; isDragging?: boolean }) => {
const wasDragging = isDragging;
isDragging = event.isDragging ?? isDragging;
clientY = event.clientY;
if (wasDragging === false && isDragging) {
scrollTimeline();
}
segmentScrollbarLayout = result;
}
const handleMouseMove = (e: MouseEvent, currentDate: Date) => {
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;
}
if (!isDragging || isAnimating) {
return;
}
isAnimating = true;
window.requestAnimationFrame(() => {
scrollTimeline();
isAnimating = false;
});
};
</script>
<svelte:window bind:innerHeight={windowHeight} />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id="immich-scrubbable-scrollbar"
class="fixed right-0 z-[100] select-none bg-immich-bg hover:cursor-row-resize"
style:width={isDragging ? '100vw' : '60px'}
style:background-color={isDragging ? 'transparent' : 'transparent'}
on:mouseenter={() => (isHover = true)}
on:mouseleave={() => {
isHover = false;
isDragging = false;
}}
on:mouseup={handleMouseUp}
on:mousemove={handleMouseDrag}
on:mousedown={handleMouseDown}
style:height={scrollbarHeight + 'px'}
>
{#if isHover}
<div
class="pointer-events-none absolute right-0 z-[100] w-[100px] rounded-tl-md border-b-2 border-immich-primary bg-immich-bg py-1 pl-1 pr-6 text-sm font-medium shadow-lg dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg"
style:top={currentMouseYLocation + 'px'}
>
{hoveredDate?.toLocaleString('default', { month: 'short' })}
{hoveredDate?.getFullYear()}
</div>
{/if}
<!-- Scroll Position Indicator Line -->
{#if !isDragging}
<div
class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
style:top={scrollbarPosition + 'px'}
/>
{/if}
<!-- Time Segment -->
{#each segmentScrollbarLayout as segment, index (segment.timeGroup)}
{@const groupDate = new Date(segment.timeGroup)}
{#if $assetStore.timelineHeight > height}
<div
id="immich-scrubbable-scrollbar"
class="fixed right-0 z-[100] select-none bg-immich-bg hover:cursor-row-resize"
style:width={isDragging ? '100vw' : '60px'}
style:height={height + 'px'}
style:background-color={isDragging ? 'transparent' : 'transparent'}
draggable="false"
on:mouseenter={() => (isHover = true)}
on:mouseleave={() => {
isHover = false;
isDragging = false;
}}
on:mouseenter={({ clientY, buttons }) => handleMouseEvent({ clientY, isDragging: !!buttons })}
on:mousemove={({ clientY }) => handleMouseEvent({ clientY })}
on:mousedown={({ clientY }) => handleMouseEvent({ clientY, isDragging: true })}
on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
>
{#if isHover}
<div
class="pointer-events-none absolute right-0 z-[100] w-[100px] rounded-tl-md border-b-2 border-immich-primary bg-immich-bg py-1 pl-1 pr-6 text-sm font-medium shadow-lg dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg"
style:top="{Math.max(hoverY - HOVER_DATE_HEIGHT, 0)}px"
>
{hoverLabel}
</div>
{/if}
<div
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()}
{#if segment.height > 8}
<!-- Scroll Position Indicator Line -->
{#if !isDragging}
<div
class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
style:top="{scrollY}px"
/>
{/if}
<!-- Time Segment -->
{#each segments as segment, index (segment.timeGroup)}
{@const date = new Date(segment.timeGroup)}
{@const year = date.getFullYear()}
{@const label = `${date.toLocaleString('default', { month: 'short' })} ${year}`}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id="time-segment"
class="relative"
style:height={segment.height + 'px'}
aria-label={segment.timeGroup + ' ' + segment.count}
on:mousemove={() => (hoverLabel = label)}
>
{#if new Date(segments[index - 1]?.timeGroup).getFullYear() !== year}
{#if segment.height > 8}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 z-10 pr-5 text-xs font-medium dark:text-immich-dark-fg"
>
{year}
</div>
{/if}
{:else if segment.height > 5}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 z-10 pr-5 text-xs font-medium dark:text-immich-dark-fg"
>
{groupDate.getFullYear()}
</div>
class="absolute right-0 mr-3 block h-[4px] w-[4px] rounded-full bg-gray-300"
/>
{/if}
{:else if segment.height > 5}
<div
aria-label={segment.timeGroup + ' ' + segment.count}
class="absolute right-0 mr-3 block h-[4px] w-[4px] rounded-full bg-gray-300"
/>
{/if}
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
<style>
#immich-scrubbable-scrollbar,

View file

@ -1,5 +0,0 @@
export class SegmentScrollbarLayout {
height!: number;
timeGroup!: string;
count!: number;
}