|
78 | 78 | let scrubBucketPercent = $state(0);
|
79 | 79 | let scrubBucket: { bucketDate: string | undefined } | undefined = $state();
|
80 | 80 | let scrubOverallPercent: number = $state(0);
|
| 81 | + let scrubberWidth = $state(0); |
81 | 82 |
|
82 | 83 | // 60 is the bottom spacer element at 60px
|
83 | 84 | let bottomSectionHeight = 60;
|
84 | 85 | let leadout = $state(false);
|
85 | 86 |
|
| 87 | + const maxMd = $derived(mobileDevice.maxMd); |
86 | 88 | const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
87 | 89 |
|
| 90 | + $effect(() => { |
| 91 | + assetStore.rowHeight = maxMd ? 100 : 235; |
| 92 | + }); |
| 93 | +
|
88 | 94 | const scrollTo = (top: number) => {
|
89 | 95 | element?.scrollTo({ top });
|
90 | 96 | showSkeleton = false;
|
|
162 | 168 | const updateIsScrolling = () => (assetStore.scrolling = true);
|
163 | 169 | // note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
164 | 170 | const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0);
|
165 |
| - const compensateScrollCallback = (delta: number) => element?.scrollBy(0, delta); |
| 171 | + const compensateScrollCallback = ({ delta, top }: { delta?: number; top?: number }) => { |
| 172 | + if (delta) { |
| 173 | + element?.scrollBy(0, delta); |
| 174 | + } else if (top) { |
| 175 | + element?.scrollTo({ top }); |
| 176 | + } |
| 177 | + }; |
166 | 178 | const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height);
|
167 | 179 |
|
168 | 180 | onMount(() => {
|
|
267 | 279 | bucket = assetStore.buckets[i];
|
268 | 280 | bucketHeight = assetStore.buckets[i].bucketHeight;
|
269 | 281 | }
|
| 282 | +
|
270 | 283 | let next = top - bucketHeight * maxScrollPercent;
|
271 |
| - if (next < 0) { |
| 284 | + // instead of checking for < 0, add a little wiggle room for subpixel resolution |
| 285 | + if (next < -1 && bucket) { |
272 | 286 | scrubBucket = bucket;
|
273 |
| - scrubBucketPercent = top / (bucketHeight * maxScrollPercent); |
| 287 | +
|
| 288 | + // allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage |
| 289 | + scrubBucketPercent = Math.max(0, top / (bucketHeight * maxScrollPercent)); |
| 290 | +
|
| 291 | + // compensate for lost precision/rouding errors advance to the next bucket, if present |
| 292 | + if (scrubBucketPercent > 0.9999 && i + 1 < bucketsLength - 1) { |
| 293 | + scrubBucket = assetStore.buckets[i + 1]; |
| 294 | + scrubBucketPercent = 0; |
| 295 | + } |
| 296 | +
|
274 | 297 | found = true;
|
275 | 298 | break;
|
276 | 299 | }
|
|
689 | 712 |
|
690 | 713 | {#if assetStore.buckets.length > 0}
|
691 | 714 | <Scrubber
|
692 |
| - invisible={showSkeleton} |
693 | 715 | {assetStore}
|
694 | 716 | height={assetStore.viewportHeight}
|
695 | 717 | timelineTopOffset={assetStore.topSectionHeight}
|
|
699 | 721 | {scrubBucketPercent}
|
700 | 722 | {scrubBucket}
|
701 | 723 | {onScrub}
|
| 724 | + bind:scrubberWidth |
702 | 725 | onScrubKeyDown={(evt) => {
|
703 | 726 | evt.preventDefault();
|
704 | 727 | let amount = 50;
|
|
720 | 743 | <!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
|
721 | 744 | <section
|
722 | 745 | id="asset-grid"
|
723 |
| - class={[ |
724 |
| - 'scrollbar-hidden h-full overflow-y-auto outline-none', |
725 |
| - { 'm-0': isEmpty }, |
726 |
| - { 'ml-0': !isEmpty }, |
727 |
| - { 'mr-[60px]': !isEmpty && !usingMobileDevice }, |
728 |
| - ]} |
| 746 | + class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ml-0': !isEmpty }]} |
| 747 | + style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'} |
729 | 748 | tabindex="-1"
|
730 | 749 | bind:clientHeight={assetStore.viewportHeight}
|
731 | 750 | bind:clientWidth={null, (v) => ((assetStore.viewportWidth = v), updateSlidingWindow())}
|
|
763 | 782 | style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
764 | 783 | style:width="100%"
|
765 | 784 | >
|
766 |
| - <Skeleton height={bucket.bucketHeight} title={bucket.bucketDateFormatted} /> |
| 785 | + <Skeleton height={bucket.bucketHeight - bucket.store.headerHeight} title={bucket.bucketDateFormatted} /> |
767 | 786 | </div>
|
768 | 787 | {:else if display}
|
769 | 788 | <div
|
|
788 | 807 | </div>
|
789 | 808 | {/if}
|
790 | 809 | {/each}
|
791 |
| - <!-- <div class="h-[60px]" style:position="absolute" style:left="0" style:right="0" style:bottom="0"></div> --> |
| 810 | + <!-- spacer for leadout --> |
| 811 | + <div |
| 812 | + class="h-[60px]" |
| 813 | + style:position="absolute" |
| 814 | + style:left="0" |
| 815 | + style:right="0" |
| 816 | + style:transform={`translate3d(0,${assetStore.timelineHeight}px,0)`} |
| 817 | + ></div> |
792 | 818 | </section>
|
793 | 819 | </section>
|
794 | 820 |
|
|
0 commit comments