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;