refactor(server,web): time buckets for main timeline, archived, and favorites (1) (#3537)

* refactor: time buckets

* feat(web): use new time bucket api

* feat(web): use asset grid in archive/favorites

* chore: open api

* chore: clean up uuid validation

* refactor(web): move memory lane to photos page

* Update web/src/routes/(user)/archive/+page.svelte

Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com>

* fix: hide archived photos on main timeline

* fix: select exif info

---------

Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-08-04 17:07:15 -04:00
committed by GitHub
parent e5bdf671b5
commit c6abef186c
51 changed files with 1516 additions and 1862 deletions

View File

@@ -47,6 +47,23 @@ export enum WithProperty {
SIDECAR = 'sidecar',
}
export enum TimeBucketSize {
DAY = 'DAY',
MONTH = 'MONTH',
}
export interface TimeBucketOptions {
size: TimeBucketSize;
isArchived?: boolean;
isFavorite?: boolean;
albumId?: string;
}
export interface TimeBucketItem {
timeBucket: string;
count: number;
}
export const IAssetRepository = 'IAssetRepository';
export interface IAssetRepository {
@@ -64,4 +81,6 @@ export interface IAssetRepository {
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBuckets(userId: string, options: TimeBucketOptions): Promise<TimeBucketItem[]>;
getByTimeBucket(userId: string, timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
}

View File

@@ -10,11 +10,20 @@ import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util';
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { IAssetRepository } from './asset.repository';
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
import {
AssetIdsDto,
DownloadArchiveInfo,
DownloadDto,
DownloadResponseDto,
MemoryLaneDto,
TimeBucketAssetDto,
TimeBucketDto,
} from './dto';
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
import { MapMarkerDto } from './dto/map-marker.dto';
import { mapAsset, MapMarkerResponseDto } from './response-dto';
import { AssetResponseDto, mapAsset, MapMarkerResponseDto } from './response-dto';
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
import { TimeBucketResponseDto } from './response-dto/time-bucket-response.dto';
export enum UploadFieldName {
ASSET_DATA = 'assetData',
@@ -135,6 +144,21 @@ export class AssetService {
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
}
async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
const { userId, ...options } = dto;
const targetId = userId || authUser.id;
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [targetId]);
return this.assetRepository.getTimeBuckets(targetId, options);
}
async getByTimeBucket(authUser: AuthUserDto, dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
const { userId, timeBucket, ...options } = dto;
const targetId = userId || authUser.id;
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [targetId]);
const assets = await this.assetRepository.getByTimeBucket(targetId, timeBucket, options);
return assets.map(mapAsset);
}
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);

View File

@@ -3,3 +3,4 @@ export * from './asset-statistics.dto';
export * from './download.dto';
export * from './map-marker.dto';
export * from './memory-lane.dto';
export * from './time-bucket.dto';

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { toBoolean, ValidateUUID } from '../../domain.util';
import { TimeBucketSize } from '../asset.repository';
export class TimeBucketDto {
@IsNotEmpty()
@IsEnum(TimeBucketSize)
@ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' })
size!: TimeBucketSize;
@ValidateUUID({ optional: true })
userId?: string;
@ValidateUUID({ optional: true })
albumId?: string;
@IsOptional()
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean;
@IsOptional()
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean;
}
export class TimeBucketAssetDto extends TimeBucketDto {
@IsString()
@IsNotEmpty()
timeBucket!: string;
}

View File

@@ -3,3 +3,4 @@ export * from './asset-response.dto';
export * from './exif-response.dto';
export * from './map-marker-response.dto';
export * from './smart-info-response.dto';
export * from './time-bucket-response.dto';

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class TimeBucketResponseDto {
@ApiProperty({ type: 'string' })
timeBucket!: string;
@ApiProperty({ type: 'integer' })
count!: number;
}