diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 583980cd6e..821f2a9194 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -98,7 +98,7 @@ let showSkeleton = $state(true); let isShowSelectDate = $state(false); let scrubBucketPercent = $state(0); - let scrubBucket: { bucketDate: string | undefined } | undefined = $state(); + let scrubBucket: { year: number; month: number } | undefined = $state(); let scrubOverallPercent: number = $state(0); let scrubberWidth = $state(0); @@ -256,7 +256,7 @@ // note: don't throttle, debounch, or otherwise make this function async - it causes flicker const onScrub: ScrubberListener = ( - bucketDate: string | undefined, + bucketDate: { year: number; month: number } | undefined, scrollPercent: number, bucketScrollPercent: number, ) => { @@ -269,7 +269,9 @@ } element.scrollTop = offset; } else { - const bucket = assetStore.buckets.find((b) => b.bucketDate === bucketDate); + const bucket = assetStore.buckets.find( + (bucket) => bucket.year === bucketDate.year && bucket.month === bucketDate.month, + ); if (!bucket) { return; } @@ -309,7 +311,7 @@ const bucketsLength = assetStore.buckets.length; for (let i = -1; i < bucketsLength + 1; i++) { - let bucket: { bucketDate: string | undefined } | undefined; + let bucket: { year: number; month: number } | undefined; let bucketHeight = 0; if (i === -1) { // lead-in @@ -585,7 +587,7 @@ break; } if (started) { - await assetStore.loadBucket(bucket.bucketDate); + await assetStore.loadBucket({ year: bucket.year, month: bucket.month }); for (const asset of bucket.getAssets()) { if (deselect) { assetInteraction.removeAssetFromMultiselectGroup(asset.id); diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte index 26a2e3f143..c18c7c83b0 100644 --- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -3,10 +3,9 @@ import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { getTabbable } from '$lib/utils/focus-util'; - import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util'; + import { type ScrubberListener } from '$lib/utils/timeline-util'; import { mdiPlay } from '@mdi/js'; import { clamp } from 'lodash-es'; - import { DateTime } from 'luxon'; import { onMount } from 'svelte'; import { fade, fly } from 'svelte/transition'; @@ -17,7 +16,7 @@ assetStore: AssetStore; scrubOverallPercent?: number; scrubBucketPercent?: number; - scrubBucket?: { bucketDate: string | undefined }; + scrubBucket?: { year: number; month: number }; leadout?: boolean; scrubberWidth?: number; onScrub?: ScrubberListener; @@ -81,7 +80,7 @@ }); const toScrollFromBucketPercentage = ( - scrubBucket: { bucketDate: string | undefined } | undefined, + scrubBucket: { year: number; month: number } | undefined, scrubBucketPercent: number, scrubOverallPercent: number, ) => { @@ -89,7 +88,7 @@ let offset = relativeTopOffset; let match = false; for (const segment of segments) { - if (segment.bucketDate === scrubBucket.bucketDate) { + if (segment.month === scrubBucket.month && segment.year === scrubBucket.year) { offset += scrubBucketPercent * segment.height; match = true; break; @@ -120,8 +119,8 @@ count: number; height: number; dateFormatted: string; - bucketDate: string; - date: DateTime; + year: number; + month: number; hasLabel: boolean; hasDot: boolean; }; @@ -141,9 +140,9 @@ top, count: bucket.assetCount, height: toScrollY(scrollBarPercentage), - bucketDate: bucket.bucketDate, - date: fromLocalDateTime(bucket.bucketDate), dateFormatted: bucket.bucketDateFormattted, + year: bucket.year, + month: bucket.month, hasLabel: false, hasDot: false, }; @@ -153,7 +152,7 @@ segment.hasLabel = true; previousLabeledSegment = segment; } else { - if (previousLabeledSegment?.date?.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) { + if (previousLabeledSegment?.year !== segment.year && height > MIN_YEAR_LABEL_DISTANCE) { height = 0; segment.hasLabel = true; previousLabeledSegment = segment; @@ -182,7 +181,13 @@ } return activeSegment?.dataset.label; }); - const bucketDate = $derived(activeSegment?.dataset.timeSegmentBucketDate); + const bucketDate = $derived.by(() => { + if (!activeSegment?.dataset.timeSegmentBucketDate) { + return undefined; + } + const [year, month] = activeSegment.dataset.timeSegmentBucketDate.split('-').map(Number); + return { year, month }; + }); const scrollSegment = $derived.by(() => { const y = scrollY; let cur = relativeTopOffset; @@ -289,12 +294,12 @@ const scrollPercent = toTimelineY(hoverY); if (wasDragging === false && isDragging) { - void startScrub?.(bucketDate, scrollPercent, bucketPercentY); - void onScrub?.(bucketDate, scrollPercent, bucketPercentY); + void startScrub?.(bucketDate!, scrollPercent, bucketPercentY); + void onScrub?.(bucketDate!, scrollPercent, bucketPercentY); } if (wasDragging && !isDragging) { - void stopScrub?.(bucketDate, scrollPercent, bucketPercentY); + void stopScrub?.(bucketDate!, scrollPercent, bucketPercentY); return; } @@ -302,7 +307,7 @@ return; } - void onScrub?.(bucketDate, scrollPercent, bucketPercentY); + void onScrub?.(bucketDate!, scrollPercent, bucketPercentY); }; const getTouch = (event: TouchEvent) => { if (event.touches.length === 1) { @@ -404,7 +409,7 @@ } if (next) { event.preventDefault(); - void onScrub?.(next.bucketDate, -1, 0); + void onScrub?.({ year: next.year, month: next.month }, -1, 0); return true; } } @@ -414,7 +419,7 @@ const next = segments[idx + 1]; if (next) { event.preventDefault(); - void onScrub?.(next.bucketDate, -1, 0); + void onScrub?.({ year: next.year, month: next.month }, -1, 0); return true; } } @@ -517,7 +522,7 @@ class="relative" style:height={relativeTopOffset + 'px'} data-id="lead-in" - data-time-segment-bucket-date={segments.at(0)?.date} + data-time-segment-bucket-date={segments.at(0)?.year + '-' + segments.at(0)?.month} data-label={segments.at(0)?.dateFormatted} > {#if relativeTopOffset > 6} @@ -525,18 +530,18 @@ {/if} - {#each segments as segment (segment.date)} + {#each segments as segment (segment.year + '-' + segment.month)}
{#if !usingMobileDevice} {#if segment.hasLabel}
- {segment.date.year} + {segment.year}
{/if} {#if segment.hasDot} diff --git a/web/src/lib/stores/assets-store.spec.ts b/web/src/lib/stores/assets-store.spec.ts index 96536757d2..fe64554b3c 100644 --- a/web/src/lib/stores/assets-store.spec.ts +++ b/web/src/lib/stores/assets-store.spec.ts @@ -14,13 +14,13 @@ describe('AssetStore', () => { const bucketAssets: Record = { '2024-03-01T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-03-01T00:00:00.000Z') })), '2024-02-01T00:00:00.000Z': timelineAssetFactory .buildList(100) - .map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-02-01T00:00:00.000Z') })), '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-01-01T00:00:00.000Z') })), }; const bucketAssetsResponse: Record = Object.fromEntries( @@ -47,15 +47,16 @@ describe('AssetStore', () => { it('calculates bucket height', () => { const plainBuckets = assetStore.buckets.map((bucket) => ({ - bucketDate: bucket.bucketDate, + year: bucket.year, + month: bucket.month, bucketHeight: bucket.bucketHeight, })); expect(plainBuckets).toEqual( expect.arrayContaining([ - expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 185.5 }), - expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 12_016 }), - expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), + expect.objectContaining({ year: 2024, month: 3, bucketHeight: 185.5 }), + expect.objectContaining({ year: 2024, month: 2, bucketHeight: 12_016 }), + expect.objectContaining({ year: 2024, month: 1, bucketHeight: 286 }), ]), ); }); @@ -70,10 +71,10 @@ describe('AssetStore', () => { const bucketAssets: Record = { '2024-01-03T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-03-01T00:00:00.000Z') })), '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-01-01T00:00:00.000Z') })), }; const bucketAssetsResponse: Record = Object.fromEntries( Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]), @@ -96,46 +97,46 @@ describe('AssetStore', () => { it('loads a bucket', async () => { expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(0); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(3); }); it('ignores invalid buckets', async () => { - await assetStore.loadBucket('2023-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2023, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(0); }); it('cancels bucket loading', async () => { const bucket = assetStore.getBucketByDate(2024, 1)!; - void assetStore.loadBucket(bucket!.bucketDate); + void assetStore.loadBucket({ year: 2024, month: 1 }); const abortSpy = vi.spyOn(bucket!.loader!.cancelToken!, 'abort'); bucket?.cancel(); expect(abortSpy).toBeCalledTimes(1); - await assetStore.loadBucket(bucket!.bucketDate); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(3); }); it('prevents loading buckets multiple times', async () => { await Promise.all([ - assetStore.loadBucket('2024-01-01T00:00:00.000Z'), - assetStore.loadBucket('2024-01-01T00:00:00.000Z'), + assetStore.loadBucket({ year: 2024, month: 1 }), + assetStore.loadBucket({ year: 2024, month: 1 }), ]); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); }); it('allows loading a canceled bucket', async () => { const bucket = assetStore.getBucketByDate(2024, 1)!; - const loadPromise = assetStore.loadBucket(bucket!.bucketDate); + const loadPromise = assetStore.loadBucket({ year: 2024, month: 1 }); bucket.cancel(); await loadPromise; expect(bucket?.getAssets().length).toEqual(0); - await assetStore.loadBucket(bucket.bucketDate); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(bucket!.getAssets().length).toEqual(3); }); }); @@ -157,20 +158,21 @@ describe('AssetStore', () => { it('adds assets to new bucket', () => { const asset = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: new Date('2024-01-20T12:00:00.000Z'), }); assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); expect(assetStore.getAssets().length).toEqual(1); expect(assetStore.buckets[0].getAssets().length).toEqual(1); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.buckets[0].year).toEqual(2024); + expect(assetStore.buckets[0].month).toEqual(1); expect(assetStore.getAssets()[0].id).toEqual(asset.id); }); it('adds assets to existing bucket', () => { const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: new Date('2024-01-20T12:00:00.000Z'), }); assetStore.addAssets([assetOne]); assetStore.addAssets([assetTwo]); @@ -178,18 +180,19 @@ describe('AssetStore', () => { expect(assetStore.buckets.length).toEqual(1); expect(assetStore.getAssets().length).toEqual(2); expect(assetStore.buckets[0].getAssets().length).toEqual(2); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.buckets[0].year).toEqual(2024); + expect(assetStore.buckets[0].month).toEqual(1); }); it('orders assets in buckets by descending date', () => { const assetOne = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: new Date('2024-01-20T12:00:00.000Z'), }); const assetTwo = timelineAssetFactory.build({ - localDateTime: '2024-01-15T12:00:00.000Z', + localDateTime: new Date('2024-01-15T12:00:00.000Z'), }); const assetThree = timelineAssetFactory.build({ - localDateTime: '2024-01-16T12:00:00.000Z', + localDateTime: new Date('2024-01-16T12:00:00.000Z'), }); assetStore.addAssets([assetOne, assetTwo, assetThree]); @@ -202,15 +205,20 @@ describe('AssetStore', () => { }); it('orders buckets by descending date', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' }); - const assetThree = timelineAssetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ localDateTime: new Date('2024-01-20T12:00:00.000Z') }); + const assetTwo = timelineAssetFactory.build({ localDateTime: new Date('2024-04-20T12:00:00.000Z') }); + const assetThree = timelineAssetFactory.build({ localDateTime: new Date('2023-01-20T12:00:00.000Z') }); assetStore.addAssets([assetOne, assetTwo, assetThree]); expect(assetStore.buckets.length).toEqual(3); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-04-01T00:00:00.000Z'); - expect(assetStore.buckets[1].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); - expect(assetStore.buckets[2].bucketDate).toEqual('2023-01-01T00:00:00.000Z'); + expect(assetStore.buckets[0].year).toEqual(2024); + expect(assetStore.buckets[0].month).toEqual(4); + + expect(assetStore.buckets[1].year).toEqual(2024); + expect(assetStore.buckets[1].month).toEqual(1); + + expect(assetStore.buckets[2].year).toEqual(2023); + expect(assetStore.buckets[2].month).toEqual(1); }); it('updates existing asset', () => { @@ -266,8 +274,8 @@ describe('AssetStore', () => { }); it('asset moves buckets when asset date changes', () => { - const asset = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' }; + const asset = timelineAssetFactory.build({ localDateTime: new Date('2024-01-20T12:00:00.000Z') }); + const updatedAsset = { ...asset, localDateTime: new Date('2024-03-20T12:00:00.000Z') }; assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); @@ -294,7 +302,7 @@ describe('AssetStore', () => { }); it('ignores invalid IDs', () => { - assetStore.addAssets(timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' })); + assetStore.addAssets(timelineAssetFactory.buildList(2, { localDateTime: new Date('2024-01-20T12:00:00.000Z') })); assetStore.removeAssets(['', 'invalid', '4c7d9acc']); expect(assetStore.getAssets().length).toEqual(2); @@ -304,7 +312,7 @@ describe('AssetStore', () => { it('removes asset from bucket', () => { const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: new Date('2024-01-20T12:00:00.000Z'), }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetOne.id]); @@ -315,7 +323,7 @@ describe('AssetStore', () => { }); it('does not remove bucket when empty', () => { - const assets = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }); + const assets = timelineAssetFactory.buildList(2, { localDateTime: new Date('2024-01-20T12:00:00.000Z') }); assetStore.addAssets(assets); assetStore.removeAssets(assets.map((asset) => asset.id)); @@ -339,10 +347,10 @@ describe('AssetStore', () => { it('populated store returns first asset', () => { const assetOne = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: new Date('2024-01-20T12:00:00.000Z'), }); const assetTwo = timelineAssetFactory.build({ - localDateTime: '2024-01-15T12:00:00.000Z', + localDateTime: new Date('2024-01-15T12:00:00.000Z'), }); assetStore.addAssets([assetOne, assetTwo]); expect(assetStore.getFirstAsset()).toEqual(assetOne); @@ -354,13 +362,13 @@ describe('AssetStore', () => { const bucketAssets: Record = { '2024-03-01T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-03-01T00:00:00.000Z') })), '2024-02-01T00:00:00.000Z': timelineAssetFactory .buildList(6) - .map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-02-01T00:00:00.000Z') })), '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: new Date('2024-01-01T00:00:00.000Z') })), }; const bucketAssetsResponse: Record = Object.fromEntries( Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]), @@ -383,7 +391,7 @@ describe('AssetStore', () => { }); it('returns previous assetId', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); const bucket = assetStore.getBucketByDate(2024, 1); const a = bucket!.getAssets()[0]; @@ -393,8 +401,8 @@ describe('AssetStore', () => { }); it('returns previous assetId spanning multiple buckets', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 2 }); + await assetStore.loadBucket({ year: 2024, month: 3 }); const bucket = assetStore.getBucketByDate(2024, 2); const previousBucket = assetStore.getBucketByDate(2024, 3); @@ -405,7 +413,7 @@ describe('AssetStore', () => { }); it('loads previous bucket', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 2 }); const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket'); const bucket = assetStore.getBucketByDate(2024, 2); @@ -418,9 +426,9 @@ describe('AssetStore', () => { }); it('skips removed assets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); + await assetStore.loadBucket({ year: 2024, month: 2 }); + await assetStore.loadBucket({ year: 2024, month: 3 }); const [assetOne, assetTwo, assetThree] = assetStore.getAssets(); assetStore.removeAssets([assetTwo.id]); @@ -428,7 +436,7 @@ describe('AssetStore', () => { }); it('returns null when no more assets', async () => { - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 3 }); expect(await assetStore.getLaterAsset(assetStore.getAssets()[0])).toBeUndefined(); }); }); @@ -449,21 +457,24 @@ describe('AssetStore', () => { }); it('returns the bucket index', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ localDateTime: new Date('2024-01-20T12:00:00.000Z') }); + const assetTwo = timelineAssetFactory.build({ localDateTime: new Date('2024-02-15T12:00:00.000Z') }); assetStore.addAssets([assetOne, assetTwo]); - expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z'); - expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.month).toEqual(2); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.month).toEqual(1); }); it('ignores removed buckets', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ localDateTime: new Date('2024-01-20T12:00:00.000Z') }); + const assetTwo = timelineAssetFactory.build({ localDateTime: new Date('2024-02-15T12:00:00.000Z') }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetTwo.id]); - expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.month).toEqual(1); }); }); }); diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts index 52d2fb8df8..cba8016c3a 100644 --- a/web/src/lib/stores/assets-store.svelte.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -49,7 +49,8 @@ function updateObject(target: any, source: any): boolean { if (!source.hasOwnProperty(key)) { continue; } - if (typeof target[key] === 'object') { + const isDate = target[key] instanceof Date; + if (typeof target[key] === 'object' && !isDate) { updated = updated || updateObject(target[key], source[key]); } else { // Otherwise, directly copy the value @@ -204,7 +205,7 @@ export class AssetDateGroup { const newTime = asset.localDateTime; if (oldTime.valueOf() !== newTime.valueOf()) { const year = newTime.getUTCFullYear(); - const month = newTime.getUTCMonth(); + const month = newTime.getUTCMonth() + 1; if (this.bucket.year !== year || this.bucket.month !== month) { remove = true; moveAssets.push({ asset, year, month }); @@ -337,24 +338,27 @@ export class AssetBucket { isBucketHeightActual: boolean = $state(false); readonly bucketDateFormatted: string; - readonly bucketDate: string; readonly month: number; readonly year: number; - constructor(store: AssetStore, date: Date, initialCount: number, order: AssetOrder = AssetOrder.Desc) { + constructor( + store: AssetStore, + { year, month }: { year: number; month: number }, + initialCount: number, + order: AssetOrder = AssetOrder.Desc, + ) { this.store = store; this.#initialCount = initialCount; this.#sortOrder = order; - const bucketDateFormatted = date.toLocaleString(get(locale), { + this.month = month; + this.year = year; + const date = new Date(Date.UTC(year, month - 1)); + this.bucketDateFormatted = date.toLocaleString(get(locale), { month: 'short', year: 'numeric', timeZone: 'UTC', }); - this.bucketDate = date.toISOString(); - this.bucketDateFormatted = bucketDateFormatted; - this.month = date.getUTCMonth(); - this.year = date.getUTCFullYear(); this.loader = new CancellableTask( () => { @@ -373,7 +377,7 @@ export class AssetBucket { if (old !== newValue) { this.#intersecting = newValue; if (newValue) { - void this.store.loadBucket(this.bucketDate); + void this.store.loadBucket({ year: this.year, month: this.month }); } else { this.cancel(); } @@ -454,7 +458,6 @@ export class AssetBucket { } addAssets(bucketAssets: TimeBucketAssetResponseDto) { - const time1 = performance.now(); const addContext = new AddContext(); const people: string[] = []; for (let i = 0; i < bucketAssets.id.length; i++) { @@ -496,16 +499,13 @@ export class AssetBucket { addContext.sort(this, this.#sortOrder); - const time2 = performance.now(); - const time = time2 - time1; - console.log(`AssetBucket.addAssets took ${time}ms`); return addContext.unprocessedAssets; } addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) { const { localDateTime } = timelineAsset; - const month = localDateTime.getUTCMonth(); + const month = localDateTime.getUTCMonth() + 1; const year = localDateTime.getUTCFullYear(); if (this.month !== month || this.year !== year) { @@ -541,7 +541,7 @@ export class AssetBucket { /** The svelte key for this view model object */ get viewId() { - return this.bucketDate; + return this.year + '-' + this.month; } set bucketHeight(height: number) { @@ -675,7 +675,8 @@ type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | Update export type LiteBucket = { bucketHeight: number; assetCount: number; - bucketDate: string; + year: number; + month: number; bucketDateFormattted: string; }; @@ -914,8 +915,9 @@ export class AssetStore { } #findBucketForDate(date: Date) { + const targetMonth = date.getUTCMonth() + 1; for (const bucket of this.buckets) { - if (bucket.month === date.getUTCMonth() && bucket.year === date.getUTCFullYear()) { + if (bucket.month === targetMonth && bucket.year === date.getUTCFullYear()) { return bucket; } } @@ -1018,7 +1020,13 @@ export class AssetStore { }); this.buckets = timebuckets.map((bucket) => { - return new AssetBucket(this, new Date(bucket.timeBucket), bucket.count, this.#options.order); + const date = new Date(bucket.timeBucket); + return new AssetBucket( + this, + { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 }, + bucket.count, + this.#options.order, + ); }); this.albumAssets.clear(); this.#updateViewportGeometry(false); @@ -1103,7 +1111,8 @@ export class AssetStore { #createScrubBuckets() { this.scrubberBuckets = this.buckets.map((bucket) => ({ assetCount: bucket.bucketCount, - bucketDate: bucket.bucketDate, + year: bucket.year, + month: bucket.month, bucketDateFormattted: bucket.bucketDateFormatted, bucketHeight: bucket.bucketHeight, })); @@ -1195,15 +1204,13 @@ export class AssetStore { bucket.isBucketHeightActual = true; } - async loadBucket(bucketDate: string, options?: { cancelable: boolean }): Promise { + // Month is 1-indexed + async loadBucket({ year, month }: { year: number; month: number }, options?: { cancelable: boolean }): Promise { let cancelable = true; if (options) { cancelable = options.cancelable; } - const date = new Date(bucketDate); - const year = date.getUTCFullYear(); - const month = date.getUTCMonth(); const bucket = this.getBucketByDate(year, month); if (!bucket) { return; @@ -1222,8 +1229,7 @@ export class AssetStore { const bucketResponse = await getTimeBucket( { ...this.#options, - timeBucket: bucketDate, - + timeBucket: new Date(Date.UTC(year, month - 1)).toISOString(), key: authManager.key, }, { signal }, @@ -1233,7 +1239,7 @@ export class AssetStore { const albumAssets = await getTimeBucket( { albumId: this.#options.timelineAlbumId, - timeBucket: bucketDate, + timeBucket: new Date(Date.UTC(year, month - 1)).toISOString(), key: authManager.key, }, { signal }, @@ -1245,7 +1251,7 @@ export class AssetStore { const unprocessed = bucket.addAssets(bucketResponse); if (unprocessed.length > 0) { console.error( - `Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`, + `Warning: getTimeBucket API returning assets not in requested month: ${bucket.month}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`, ); } this.#layoutBucket(bucket); @@ -1280,11 +1286,11 @@ export class AssetStore { const bucketCount = this.buckets.length; for (const asset of assets) { const year = asset.localDateTime.getUTCFullYear(); - const month = asset.localDateTime.getUTCMonth(); + const month = asset.localDateTime.getUTCMonth() + 1; let bucket = this.getBucketByDate(year, month); if (!bucket) { - bucket = new AssetBucket(this, asset.localDateTime, 1, this.#options.order); + bucket = new AssetBucket(this, { year, month }, 1, this.#options.order); this.buckets.push(bucket); } @@ -1325,7 +1331,10 @@ export class AssetStore { if (!asset || this.isExcluded(asset)) { return; } - bucket = await this.#loadBucketAtTime(asset.localDateTime, { cancelable: false }); + const { localDateTime } = asset; + const year = localDateTime.getUTCFullYear(); + const month = localDateTime.getUTCMonth() + 1; + bucket = await this.#loadBucketAtTime({ year, month }, { cancelable: false }); } if (bucket && bucket?.containsAssetId(id)) { @@ -1333,12 +1342,9 @@ export class AssetStore { } } - async #loadBucketAtTime(localDateTime: Date, options?: { cancelable: boolean }) { + async #loadBucketAtTime({ year, month }: { year: number; month: number }, options?: { cancelable: boolean }) { // Only support TimeBucketSize.Month - const year = localDateTime.getUTCFullYear(); - const month = localDateTime.getUTCMonth(); - localDateTime = new Date(year, month); - await this.loadBucket(localDateTime.toISOString(), options); + await this.loadBucket({ year, month }, options); return this.getBucketByDate(year, month); } @@ -1349,7 +1355,7 @@ export class AssetStore { async getRandomBucket() { const random = Math.floor(Math.random() * this.buckets.length); const bucket = this.buckets[random]; - await this.loadBucket(bucket.bucketDate, { cancelable: false }); + await this.loadBucket({ year: bucket.year, month: bucket.month }, { cancelable: false }); return bucket; } @@ -1456,7 +1462,7 @@ export class AssetStore { if (!bucket) { return; } - await this.loadBucket(bucket.bucketDate, { cancelable: false }); + await this.loadBucket({ year: bucket.year, month: bucket.month }, { cancelable: false }); const asset = bucket.findClosest(date); if (asset) { return asset; @@ -1465,7 +1471,7 @@ export class AssetStore { const startIndex = this.buckets.indexOf(bucket); for (let currentIndex = startIndex + 1; currentIndex < this.buckets.length; currentIndex++) { bucket = this.buckets[currentIndex]; - await this.loadBucket(bucket.bucketDate); + await this.loadBucket({ year: bucket.year, month: bucket.month }, { cancelable: false }); const next = bucket.dateGroups[0]?.intersetingAssets[0]?.asset; if (next) { return next; @@ -1525,7 +1531,7 @@ export class AssetStore { for (let currentIndex = startIndex + increment; endCondition(currentIndex); currentIndex += increment) { const targetBucket = this.buckets[currentIndex]; - await this.loadBucket(targetBucket.bucketDate, { cancelable: false }); + await this.loadBucket({ year: targetBucket.year, month: targetBucket.month }, { cancelable: false }); if (targetBucket.dateGroups.length > 0) { return targetBucket.dateGroups[0]?.intersetingAssets[0]?.asset; } @@ -1539,7 +1545,7 @@ export class AssetStore { const targetBucket = this.buckets[targetBucketIndex]; if (targetBucket) { - await this.loadBucket(targetBucket.bucketDate, { cancelable: false }); + await this.loadBucket({ year: targetBucket.year, month: targetBucket.month }, { cancelable: false }); return targetBucket.dateGroups[0]?.intersetingAssets[0]?.asset; } return; @@ -1558,7 +1564,7 @@ export class AssetStore { for (let currentIndex = startIndex; endCondition(currentIndex); currentIndex += increment) { const otherBucket = this.buckets[currentIndex]; - const otherBucketYear = DateTime.fromISO(otherBucket.bucketDate).toUTC().get('year'); + const otherBucketYear = otherBucket.year; const yearCondition = direction === 'forward' @@ -1566,7 +1572,7 @@ export class AssetStore { : otherBucketYear <= targetYear; // Looking for older years if (yearCondition) { - await this.loadBucket(otherBucket.bucketDate, { cancelable: false }); + await this.loadBucket({ year: otherBucket.year, month: otherBucket.month }, { cancelable: false }); return otherBucket.dateGroups[0]?.intersetingAssets[0]?.asset; } } @@ -1608,7 +1614,7 @@ export class AssetStore { for (let currentIndex = startIndex + increment; endCondition(currentIndex); currentIndex += increment) { const adjacentBucket = this.buckets[currentIndex]; - await this.loadBucket(adjacentBucket.bucketDate); + await this.loadBucket({ year: adjacentBucket.year, month: adjacentBucket.month }, { cancelable: false }); if (adjacentBucket.dateGroups.length > 0) { return direction === 'forward' diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 99d4162498..465c8405ef 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -479,7 +479,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: try { for (const bucket of assetStore.buckets) { - await assetStore.loadBucket(bucket.bucketDate); + await assetStore.loadBucket({ year: bucket.year, month: bucket.month }); if (!get(isSelectingAllAssets)) { assetInteraction.clearMultiselect(); diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts index f3f9d51fad..8029be8c9c 100644 --- a/web/src/lib/utils/thumbnail-util.spec.ts +++ b/web/src/lib/utils/thumbnail-util.spec.ts @@ -61,7 +61,7 @@ describe('getAltText', () => { ownerId: 'test-owner', ratio: 1, thumbhash: null, - localDateTime: '2024-01-01T12:00:00.000Z', + localDateTime: new Date('2024-01-01T12:00:00.000Z'), visibility: AssetVisibility.Timeline, isFavorite: false, isTrashed: false, diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 2441bb44f4..2226d64616 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -9,7 +9,7 @@ import { DateTime, type LocaleOptions } from 'luxon'; import { get } from 'svelte/store'; export type ScrubberListener = ( - bucketDate: string | undefined, + bucketDate: { year: number; month: number }, overallScrollPercent: number, bucketScrollPercent: number, ) => void | Promise;