mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
fix: memory lane assets in ascending order (#8309)
* fix: memory lane asset order * chore: deprecate title * chore: open-api * chore: rename years => yearsAgo
This commit is contained in:
parent
13b11a39a9
commit
9fe80c25eb
12 changed files with 48 additions and 26 deletions
BIN
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
BIN
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
BIN
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
BIN
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
Binary file not shown.
|
@ -8427,12 +8427,17 @@
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
"deprecated": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"yearsAgo": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"assets",
|
"assets",
|
||||||
"title"
|
"title",
|
||||||
|
"yearsAgo"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|
|
@ -273,6 +273,7 @@ export type MapMarkerResponseDto = {
|
||||||
export type MemoryLaneResponseDto = {
|
export type MemoryLaneResponseDto = {
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
title: string;
|
title: string;
|
||||||
|
yearsAgo: number;
|
||||||
};
|
};
|
||||||
export type UpdateStackParentDto = {
|
export type UpdateStackParentDto = {
|
||||||
newParentId: string;
|
newParentId: string;
|
||||||
|
|
|
@ -131,7 +131,9 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemoryLaneResponseDto {
|
export class MemoryLaneResponseDto {
|
||||||
|
@ApiProperty({ deprecated: true })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
yearsAgo!: number;
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ export class AssetRepository implements IAssetRepository {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
|
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
|
||||||
.orderBy('entity.localDateTime', 'DESC')
|
.orderBy('entity.localDateTime', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -307,13 +307,17 @@ describe(AssetService.name, () => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the title correctly', async () => {
|
it('should group the assets correctly', async () => {
|
||||||
|
const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) };
|
||||||
|
const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) };
|
||||||
|
const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) };
|
||||||
|
|
||||||
partnerMock.getAll.mockResolvedValue([]);
|
partnerMock.getAll.mockResolvedValue([]);
|
||||||
assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
|
assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3]);
|
||||||
|
|
||||||
await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
|
await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
|
||||||
{ title: '1 year since...', assets: [mapAsset(assetStub.image)] },
|
{ yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] },
|
||||||
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] },
|
{ yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
||||||
|
@ -321,6 +325,7 @@ describe(AssetService.name, () => {
|
||||||
|
|
||||||
it('should get memories with partners with inTimeline enabled', async () => {
|
it('should get memories with partners with inTimeline enabled', async () => {
|
||||||
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||||
|
assetMock.getByDayOfYear.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });
|
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });
|
||||||
|
|
||||||
|
|
|
@ -174,20 +174,25 @@ export class AssetService {
|
||||||
userIds.push(...partnersIds);
|
userIds.push(...partnersIds);
|
||||||
|
|
||||||
const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
|
const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
|
||||||
|
const groups: Record<number, AssetEntity[]> = {};
|
||||||
|
for (const asset of assets) {
|
||||||
|
const yearsAgo = currentYear - asset.localDateTime.getFullYear();
|
||||||
|
if (!groups[yearsAgo]) {
|
||||||
|
groups[yearsAgo] = [];
|
||||||
|
}
|
||||||
|
groups[yearsAgo].push(asset);
|
||||||
|
}
|
||||||
|
|
||||||
return _.chain(assets)
|
return Object.keys(groups)
|
||||||
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
|
.map(Number)
|
||||||
.map((asset) => {
|
.sort()
|
||||||
const years = currentYear - asset.localDateTime.getFullYear();
|
.filter((yearsAgo) => yearsAgo > 0)
|
||||||
|
.map((yearsAgo) => ({
|
||||||
return {
|
yearsAgo,
|
||||||
title: `${years} year${years > 1 ? 's' : ''} since...`,
|
// TODO move this to clients
|
||||||
asset: mapAsset(asset, { auth }),
|
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
|
||||||
};
|
assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })),
|
||||||
})
|
}));
|
||||||
.groupBy((asset) => asset.title)
|
|
||||||
.map((items, title) => ({ title, assets: items.map(({ asset }) => asset) }))
|
|
||||||
.value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import type { Viewport } from '$lib/stores/assets.store';
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
import { memoryStore } from '$lib/stores/memory.store';
|
import { memoryStore } from '$lib/stores/memory.store';
|
||||||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
||||||
import { shortcuts } from '$lib/utils/shortcut';
|
import { shortcuts } from '$lib/utils/shortcut';
|
||||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||||
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
|
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
{currentMemory.title}
|
{memoryLaneTitle(currentMemory.yearsAgo)}
|
||||||
</p>
|
</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@
|
||||||
{#if previousMemory}
|
{#if previousMemory}
|
||||||
<div class="absolute bottom-4 right-4 text-left text-white">
|
<div class="absolute bottom-4 right-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
|
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
|
||||||
<p class="text-xl">{previousMemory.title}</p>
|
<p class="text-xl">{memoryLaneTitle(previousMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
@ -254,7 +254,7 @@
|
||||||
{#if nextMemory}
|
{#if nextMemory}
|
||||||
<div class="absolute bottom-4 left-4 text-left text-white">
|
<div class="absolute bottom-4 left-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">UP NEXT</p>
|
<p class="text-xs font-semibold text-gray-200">UP NEXT</p>
|
||||||
<p class="text-xl">{nextMemory.title}</p>
|
<p class="text-xl">{memoryLaneTitle(nextMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { memoryStore } from '$lib/stores/memory.store';
|
import { memoryStore } from '$lib/stores/memory.store';
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
||||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
||||||
{#each $memoryStore as memory, index (memory.title)}
|
{#each $memoryStore as memory, index (memory.yearsAgo)}
|
||||||
<button
|
<button
|
||||||
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
|
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
|
||||||
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
|
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
|
||||||
|
@ -77,7 +77,9 @@
|
||||||
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
|
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">{memory.title}</p>
|
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">
|
||||||
|
{memoryLaneTitle(memory.yearsAgo)}
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -277,3 +277,5 @@ export const asyncTimeout = (ms: number) => {
|
||||||
export const handlePromiseError = <T>(promise: Promise<T>): void => {
|
export const handlePromiseError = <T>(promise: Promise<T>): void => {
|
||||||
promise.catch((error) => console.error(`[utils.ts]:handlePromiseError ${error}`, error));
|
promise.catch((error) => console.error(`[utils.ts]:handlePromiseError ${error}`, error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const memoryLaneTitle = (yearsAgo: number) => `${yearsAgo} ${yearsAgo ? 'years' : 'year'} since...`;
|
||||||
|
|
Loading…
Reference in a new issue