mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
Finish up removing/reducing date parsing
This commit is contained in:
parent
5db3ed8412
commit
727c9a49d9
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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'
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user