Finish up removing/reducing date parsing

This commit is contained in:
Min Idzelis 2025-05-21 01:25:21 +00:00
parent 5db3ed8412
commit 727c9a49d9
7 changed files with 153 additions and 129 deletions

View File

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

View File

@ -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}
</div>
<!-- Time Segment -->
{#each segments as segment (segment.date)}
{#each segments as segment (segment.year + '-' + segment.month)}
<div
class="relative"
data-id="time-segment"
data-time-segment-bucket-date={segment.date}
data-time-segment-bucket-date={segment.year + '-' + segment.month}
data-label={segment.dateFormatted}
style:height={segment.height + 'px'}
>
{#if !usingMobileDevice}
{#if segment.hasLabel}
<div class="absolute end-5 top-[-16px] text-[12px] dark:text-immich-dark-fg font-immich-mono">
{segment.date.year}
{segment.year}
</div>
{/if}
{#if segment.hasDot}

View File

@ -14,13 +14,13 @@ describe('AssetStore', () => {
const bucketAssets: Record<string, TimelineAsset[]> = {
'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<string, TimeBucketAssetResponseDto> = 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<string, TimelineAsset[]> = {
'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<string, TimeBucketAssetResponseDto> = 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<string, TimelineAsset[]> = {
'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<string, TimeBucketAssetResponseDto> = 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);
});
});
});

View File

@ -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<void> {
// Month is 1-indexed
async loadBucket({ year, month }: { year: number; month: number }, options?: { cancelable: boolean }): Promise<void> {
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'

View File

@ -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();

View File

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

View File

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