Adapt web client to consume new server response format
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { AbortError } from '$lib/utils';
|
||||
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { assetFactory, timelineAssetFactory } from '@test-data/factories/asset-factory';
|
||||
import { AssetStore } from './assets-store.svelte';
|
||||
import { type AssetResponseDto, type TimeBucketResponseDto } from '@immich/sdk';
|
||||
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||
import { AssetStore, type TimelineAsset } from './assets-store.svelte';
|
||||
|
||||
describe('AssetStore', () => {
|
||||
beforeEach(() => {
|
||||
@@ -11,18 +11,22 @@ describe('AssetStore', () => {
|
||||
|
||||
describe('init', () => {
|
||||
let assetStore: AssetStore;
|
||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||
'2024-03-01T00:00:00.000Z': assetFactory
|
||||
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||
'2024-03-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(1)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||
'2024-02-01T00:00:00.000Z': assetFactory
|
||||
'2024-02-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(100)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
||||
'2024-01-01T00:00:00.000Z': assetFactory
|
||||
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(3)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||
};
|
||||
|
||||
const bucketAssetsResponse: Record<string, TimeBucketResponseDto> = Object.fromEntries(
|
||||
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
assetStore = new AssetStore();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||
@@ -30,13 +34,14 @@ describe('AssetStore', () => {
|
||||
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
||||
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
||||
});
|
||||
|
||||
it('should load buckets in viewport', () => {
|
||||
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||
expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month });
|
||||
|
||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -48,29 +53,31 @@ describe('AssetStore', () => {
|
||||
|
||||
expect(plainBuckets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 304 }),
|
||||
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4515.333_333_333_333 }),
|
||||
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 186.5 }),
|
||||
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 12_017 }),
|
||||
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates timeline height', () => {
|
||||
expect(assetStore.timelineHeight).toBe(5105.333_333_333_333);
|
||||
expect(assetStore.timelineHeight).toBe(12_489.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadBucket', () => {
|
||||
let assetStore: AssetStore;
|
||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||
'2024-01-03T00:00:00.000Z': assetFactory
|
||||
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||
'2024-01-03T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(1)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||
'2024-01-01T00:00:00.000Z': assetFactory
|
||||
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(3)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||
};
|
||||
|
||||
const bucketAssetsResponse: Record<string, TimeBucketResponseDto> = Object.fromEntries(
|
||||
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||
);
|
||||
beforeEach(async () => {
|
||||
assetStore = new AssetStore();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||
@@ -82,7 +89,7 @@ describe('AssetStore', () => {
|
||||
if (signal?.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
return bucketAssets[timeBucket];
|
||||
return bucketAssetsResponse[timeBucket];
|
||||
});
|
||||
await assetStore.updateViewport({ width: 1588, height: 0 });
|
||||
});
|
||||
@@ -296,7 +303,9 @@ describe('AssetStore', () => {
|
||||
});
|
||||
|
||||
it('removes asset from bucket', () => {
|
||||
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
|
||||
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
|
||||
localDateTime: '2024-01-20T12:00:00.000Z',
|
||||
});
|
||||
assetStore.addAssets([assetOne, assetTwo]);
|
||||
assetStore.removeAssets([assetOne.id]);
|
||||
|
||||
@@ -342,17 +351,20 @@ describe('AssetStore', () => {
|
||||
|
||||
describe('getPreviousAsset', () => {
|
||||
let assetStore: AssetStore;
|
||||
const bucketAssets: Record<string, AssetResponseDto[]> = {
|
||||
'2024-03-01T00:00:00.000Z': assetFactory
|
||||
const bucketAssets: Record<string, TimelineAsset[]> = {
|
||||
'2024-03-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(1)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
|
||||
'2024-02-01T00:00:00.000Z': assetFactory
|
||||
'2024-02-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(6)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
|
||||
'2024-01-01T00:00:00.000Z': assetFactory
|
||||
'2024-01-01T00:00:00.000Z': timelineAssetFactory
|
||||
.buildList(3)
|
||||
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
|
||||
};
|
||||
const bucketAssetsResponse: Record<string, TimeBucketResponseDto> = Object.fromEntries(
|
||||
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
assetStore = new AssetStore();
|
||||
@@ -361,8 +373,7 @@ describe('AssetStore', () => {
|
||||
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
|
||||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
|
||||
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
await assetStore.updateViewport({ width: 1588, height: 1000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
getAssetInfo,
|
||||
getTimeBucket,
|
||||
getTimeBuckets,
|
||||
TimeBucketSize,
|
||||
type AssetResponseDto,
|
||||
type AssetStackResponseDto,
|
||||
type TimeBucketResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -84,7 +83,7 @@ export type TimelineAsset = {
|
||||
duration: string | null;
|
||||
projectionType: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
text: {
|
||||
description: {
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
people: string[];
|
||||
@@ -418,11 +417,34 @@ export class AssetBucket {
|
||||
};
|
||||
}
|
||||
|
||||
#decodeString(stringOrNumber: string | number) {
|
||||
return typeof stringOrNumber === 'number' ? null : (stringOrNumber as string);
|
||||
}
|
||||
|
||||
// note - if the assets are not part of this bucket, they will not be added
|
||||
addAssets(bucketResponse: AssetResponseDto[]) {
|
||||
addAssets(bucketResponse: TimeBucketResponseDto) {
|
||||
const addContext = new AddContext();
|
||||
for (const asset of bucketResponse) {
|
||||
const timelineAsset = toTimelineAsset(asset);
|
||||
for (let i = 0; i < bucketResponse.bucketAssets.id.length; i++) {
|
||||
const timelineAsset: TimelineAsset = {
|
||||
description: {
|
||||
...bucketResponse.bucketAssets.description[i],
|
||||
people: [],
|
||||
},
|
||||
duration: this.#decodeString(bucketResponse.bucketAssets.duration[i]),
|
||||
id: bucketResponse.bucketAssets.id[i],
|
||||
isArchived: !!bucketResponse.bucketAssets.isArchived[i],
|
||||
isFavorite: !!bucketResponse.bucketAssets.isFavorite[i],
|
||||
isImage: !!bucketResponse.bucketAssets.isImage[i],
|
||||
isTrashed: !!bucketResponse.bucketAssets.isTrashed[i],
|
||||
isVideo: !!bucketResponse.bucketAssets.isVideo[i],
|
||||
livePhotoVideoId: this.#decodeString(bucketResponse.bucketAssets.livePhotoVideoId[i]),
|
||||
localDateTime: bucketResponse.bucketAssets.localDateTime[i],
|
||||
ownerId: bucketResponse.bucketAssets.ownerId[i],
|
||||
projectionType: this.#decodeString(bucketResponse.bucketAssets.projectionType[i]),
|
||||
ratio: bucketResponse.bucketAssets.ratio[i],
|
||||
stack: bucketResponse.bucketAssets.stack[i],
|
||||
thumbhash: this.#decodeString(bucketResponse.bucketAssets.thumbhash[i]),
|
||||
};
|
||||
this.addTimelineAsset(timelineAsset, addContext);
|
||||
}
|
||||
|
||||
@@ -878,7 +900,6 @@ export class AssetStore {
|
||||
async #initialiazeTimeBuckets() {
|
||||
const timebuckets = await getTimeBuckets({
|
||||
...this.#options,
|
||||
size: TimeBucketSize.Month,
|
||||
key: authManager.key,
|
||||
});
|
||||
|
||||
@@ -1086,7 +1107,7 @@ export class AssetStore {
|
||||
{
|
||||
...this.#options,
|
||||
timeBucket: bucketDate,
|
||||
size: TimeBucketSize.Month,
|
||||
|
||||
key: authManager.key,
|
||||
},
|
||||
{ signal },
|
||||
@@ -1097,12 +1118,11 @@ export class AssetStore {
|
||||
{
|
||||
albumId: this.#options.timelineAlbumId,
|
||||
timeBucket: bucketDate,
|
||||
size: TimeBucketSize.Month,
|
||||
key: authManager.key,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
for (const { id } of albumAssets) {
|
||||
for (const id of albumAssets.bucketAssets.id) {
|
||||
this.albumAssets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,15 +38,10 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
|
||||
return 300;
|
||||
}
|
||||
|
||||
export const getAltTextForTimelineAsset = (_: TimelineAsset) => {
|
||||
// TODO: implement this in a performant way
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getAltText = derived(t, ($t) => {
|
||||
return (asset: TimelineAsset) => {
|
||||
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) });
|
||||
const { city, country, people: names } = asset.text;
|
||||
const { city, country, people: names } = asset.description;
|
||||
const hasPlace = city && country;
|
||||
|
||||
const peopleCount = names.length;
|
||||
|
||||
@@ -71,7 +71,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
||||
const city = assetResponse.exifInfo?.city;
|
||||
const country = assetResponse.exifInfo?.country;
|
||||
const people = assetResponse.people?.map((person) => person.name) || [];
|
||||
const text = {
|
||||
|
||||
const description = {
|
||||
city: city || null,
|
||||
country: country || null,
|
||||
people,
|
||||
@@ -91,7 +92,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
||||
duration: assetResponse.duration || null,
|
||||
projectionType: assetResponse.exifInfo?.projectionType || null,
|
||||
livePhotoVideoId: assetResponse.livePhotoVideoId || null,
|
||||
text,
|
||||
description,
|
||||
};
|
||||
};
|
||||
export const isTimelineAsset = (arg: AssetResponseDto | TimelineAsset): arg is TimelineAsset =>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
type AssetResponseDto,
|
||||
type TimeBucketAssetResponseDto,
|
||||
type TimeBucketResponseDto,
|
||||
type TimelineStackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
|
||||
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
||||
@@ -42,9 +48,51 @@ export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
||||
stack: null,
|
||||
projectionType: null,
|
||||
livePhotoVideoId: Sync.each(() => faker.string.uuid()),
|
||||
text: Sync.each(() => ({
|
||||
description: Sync.each(() => ({
|
||||
city: faker.location.city(),
|
||||
country: faker.location.country(),
|
||||
people: [faker.person.fullName()],
|
||||
})),
|
||||
});
|
||||
|
||||
export const toResponseDto = (...timelineAsset: TimelineAsset[]) => {
|
||||
const bucketAssets: TimeBucketAssetResponseDto = {
|
||||
description: [],
|
||||
duration: [],
|
||||
id: [],
|
||||
isArchived: [],
|
||||
isFavorite: [],
|
||||
isImage: [],
|
||||
isTrashed: [],
|
||||
isVideo: [],
|
||||
livePhotoVideoId: [],
|
||||
localDateTime: [],
|
||||
ownerId: [],
|
||||
projectionType: [],
|
||||
ratio: [],
|
||||
stack: [],
|
||||
thumbhash: [],
|
||||
};
|
||||
for (const asset of timelineAsset) {
|
||||
bucketAssets.description.push(asset.description);
|
||||
bucketAssets.duration.push(asset.duration || 0);
|
||||
bucketAssets.id.push(asset.id);
|
||||
bucketAssets.isArchived.push(asset.isArchived ? 1 : 0);
|
||||
bucketAssets.isFavorite.push(asset.isFavorite ? 1 : 0);
|
||||
bucketAssets.isImage.push(asset.isImage ? 1 : 0);
|
||||
bucketAssets.isTrashed.push(asset.isTrashed ? 1 : 0);
|
||||
bucketAssets.isVideo.push(asset.isVideo ? 1 : 0);
|
||||
bucketAssets.livePhotoVideoId.push(asset.livePhotoVideoId || 0);
|
||||
bucketAssets.localDateTime.push(asset.localDateTime);
|
||||
bucketAssets.ownerId.push(asset.ownerId);
|
||||
bucketAssets.projectionType.push(asset.projectionType || 0);
|
||||
bucketAssets.ratio.push(asset.ratio);
|
||||
bucketAssets.stack.push(asset.stack as TimelineStackResponseDto);
|
||||
bucketAssets.thumbhash.push(asset.thumbhash || 0);
|
||||
}
|
||||
const response: TimeBucketResponseDto = {
|
||||
bucketAssets,
|
||||
hasNextPage: false,
|
||||
};
|
||||
return response;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user