mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(web): Memory (#2759)
* Add on this day * add query for x year * dev: add query * dev: front end * dev: styling * styling * more styling * add new page * navigating * navigate back and forth * styling * show gallery * fix test * fix test * show previous and next title * fix test * show up down scrolling button * more styling * styling * fix app bar * fix height of next/previous * autoplay * auto play * refactor * refactor * refactor * show date * Navigate * finish * pr feedback
This commit is contained in:
@@ -1534,6 +1534,50 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/memory-lane": {
|
||||
"get": {
|
||||
"operationId": "getMemoryLane",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "timezone",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemoryLaneResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Asset"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/search": {
|
||||
"post": {
|
||||
"operationId": "searchAsset",
|
||||
@@ -5709,6 +5753,24 @@
|
||||
"lon"
|
||||
]
|
||||
},
|
||||
"MemoryLaneResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"assets"
|
||||
]
|
||||
},
|
||||
"OAuthCallbackDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -42,6 +42,7 @@ export enum WithProperty {
|
||||
export const IAssetRepository = 'IAssetRepository';
|
||||
|
||||
export interface IAssetRepository {
|
||||
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
|
||||
getByIds(ids: string[]): Promise<AssetEntity[]>;
|
||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||
getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Inject } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IAssetRepository } from './asset.repository';
|
||||
import { MapMarkerDto } from './dto/map-marker.dto';
|
||||
import { MapMarkerResponseDto } from './response-dto';
|
||||
import { MapMarkerResponseDto, mapAsset } from './response-dto';
|
||||
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export class AssetService {
|
||||
constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
|
||||
@@ -10,4 +12,31 @@ export class AssetService {
|
||||
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||
return this.assetRepository.getMapMarkers(authUser.id, options);
|
||||
}
|
||||
|
||||
async getMemoryLane(authUser: AuthUserDto, timezone: string): Promise<MemoryLaneResponseDto[]> {
|
||||
const result: MemoryLaneResponseDto[] = [];
|
||||
|
||||
const luxonDate = DateTime.fromISO(new Date().toISOString(), { zone: timezone });
|
||||
const today = new Date(luxonDate.year, luxonDate.month - 1, luxonDate.day);
|
||||
|
||||
const years = Array.from({ length: 30 }, (_, i) => {
|
||||
const year = today.getFullYear() - i - 1;
|
||||
return new Date(year, today.getMonth(), today.getDate());
|
||||
});
|
||||
|
||||
for (const year of years) {
|
||||
const assets = await this.assetRepository.getByDate(authUser.id, year);
|
||||
|
||||
if (assets.length > 0) {
|
||||
const yearGap = today.getFullYear() - year.getFullYear();
|
||||
const memory = new MemoryLaneResponseDto();
|
||||
memory.title = `${yearGap} year${yearGap > 1 && 's'} since...`;
|
||||
memory.assets = assets.map((a) => mapAsset(a));
|
||||
|
||||
result.push(memory);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { AssetResponseDto } from './asset-response.dto';
|
||||
|
||||
export class MemoryLaneResponseDto {
|
||||
title!: string;
|
||||
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
||||
import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller('asset')
|
||||
@@ -17,4 +18,12 @@ export class AssetController {
|
||||
getMapMarkers(@GetAuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||
return this.service.getMapMarkers(authUser, options);
|
||||
}
|
||||
|
||||
@Get('memory-lane')
|
||||
getMemoryLane(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Query('timezone') timezone: string,
|
||||
): Promise<MemoryLaneResponseDto[]> {
|
||||
return this.service.getMemoryLane(authUser, timezone);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,40 @@ import { paginate } from '../utils/pagination.util';
|
||||
export class AssetRepository implements IAssetRepository {
|
||||
constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
|
||||
|
||||
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
|
||||
// For reference of a correct approach althought slower
|
||||
|
||||
// let builder = this.repository
|
||||
// .createQueryBuilder('asset')
|
||||
// .leftJoin('asset.exifInfo', 'exifInfo')
|
||||
// .where('asset.ownerId = :ownerId', { ownerId })
|
||||
// .andWhere(
|
||||
// `coalesce(date_trunc('day', asset."fileCreatedAt", "exifInfo"."timeZone") at TIME ZONE "exifInfo"."timeZone", date_trunc('day', asset."fileCreatedAt")) IN (:date)`,
|
||||
// { date },
|
||||
// )
|
||||
// .andWhere('asset.isVisible = true')
|
||||
// .andWhere('asset.isArchived = false')
|
||||
// .orderBy('asset.fileCreatedAt', 'DESC');
|
||||
|
||||
// return builder.getMany();
|
||||
const tomorrow = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
isVisible: true,
|
||||
isArchived: false,
|
||||
fileCreatedAt: OptionalBetween(date, tomorrow),
|
||||
},
|
||||
relations: {
|
||||
exifInfo: true,
|
||||
},
|
||||
order: {
|
||||
fileCreatedAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getByIds(ids: string[]): Promise<AssetEntity[]> {
|
||||
return this.repository.find({
|
||||
where: { id: In(ids) },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IAssetRepository } from '@app/domain';
|
||||
|
||||
export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
||||
return {
|
||||
getByDate: jest.fn(),
|
||||
getByIds: jest.fn().mockResolvedValue([]),
|
||||
getWithout: jest.fn(),
|
||||
getWith: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user