mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
Merge a0ffa6f90c into 72f5ca4420
This commit is contained in:
commit
783bf4d2b7
16 changed files with 360 additions and 24 deletions
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
|
|
@ -340,6 +340,7 @@ Class | Method | HTTP request | Description
|
|||
- [AssetMetadataUpsertDto](doc//AssetMetadataUpsertDto.md)
|
||||
- [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md)
|
||||
- [AssetOrder](doc//AssetOrder.md)
|
||||
- [AssetOrderBy](doc//AssetOrderBy.md)
|
||||
- [AssetResponseDto](doc//AssetResponseDto.md)
|
||||
- [AssetStackResponseDto](doc//AssetStackResponseDto.md)
|
||||
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
||||
|
|
|
|||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
|
@ -111,6 +111,7 @@ part 'model/asset_metadata_response_dto.dart';
|
|||
part 'model/asset_metadata_upsert_dto.dart';
|
||||
part 'model/asset_metadata_upsert_item_dto.dart';
|
||||
part 'model/asset_order.dart';
|
||||
part 'model/asset_order_by.dart';
|
||||
part 'model/asset_response_dto.dart';
|
||||
part 'model/asset_stack_response_dto.dart';
|
||||
part 'model/asset_stats_response_dto.dart';
|
||||
|
|
|
|||
30
mobile/openapi/lib/api/timeline_api.dart
generated
30
mobile/openapi/lib/api/timeline_api.dart
generated
|
|
@ -39,6 +39,9 @@ class TimelineApi {
|
|||
/// * [AssetOrder] order:
|
||||
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
|
||||
///
|
||||
/// * [AssetOrderBy] orderBy:
|
||||
/// The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)
|
||||
///
|
||||
/// * [String] personId:
|
||||
/// Filter assets containing a specific person (face recognition)
|
||||
///
|
||||
|
|
@ -61,7 +64,7 @@ class TimelineApi {
|
|||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/timeline/bucket';
|
||||
|
||||
|
|
@ -87,6 +90,9 @@ class TimelineApi {
|
|||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (orderBy != null) {
|
||||
queryParams.addAll(_queryParams('', 'orderBy', orderBy));
|
||||
}
|
||||
if (personId != null) {
|
||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||
}
|
||||
|
|
@ -148,6 +154,9 @@ class TimelineApi {
|
|||
/// * [AssetOrder] order:
|
||||
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
|
||||
///
|
||||
/// * [AssetOrderBy] orderBy:
|
||||
/// The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)
|
||||
///
|
||||
/// * [String] personId:
|
||||
/// Filter assets containing a specific person (face recognition)
|
||||
///
|
||||
|
|
@ -170,8 +179,8 @@ class TimelineApi {
|
|||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
|
@ -205,6 +214,9 @@ class TimelineApi {
|
|||
/// * [AssetOrder] order:
|
||||
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
|
||||
///
|
||||
/// * [AssetOrderBy] orderBy:
|
||||
/// The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)
|
||||
///
|
||||
/// * [String] personId:
|
||||
/// Filter assets containing a specific person (face recognition)
|
||||
///
|
||||
|
|
@ -227,7 +239,7 @@ class TimelineApi {
|
|||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/timeline/buckets';
|
||||
|
||||
|
|
@ -253,6 +265,9 @@ class TimelineApi {
|
|||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (orderBy != null) {
|
||||
queryParams.addAll(_queryParams('', 'orderBy', orderBy));
|
||||
}
|
||||
if (personId != null) {
|
||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||
}
|
||||
|
|
@ -310,6 +325,9 @@ class TimelineApi {
|
|||
/// * [AssetOrder] order:
|
||||
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
|
||||
///
|
||||
/// * [AssetOrderBy] orderBy:
|
||||
/// The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)
|
||||
///
|
||||
/// * [String] personId:
|
||||
/// Filter assets containing a specific person (face recognition)
|
||||
///
|
||||
|
|
@ -332,8 +350,8 @@ class TimelineApi {
|
|||
///
|
||||
/// * [bool] withStacked:
|
||||
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
|
||||
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
|
||||
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
|
|
|||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
|
@ -276,6 +276,8 @@ class ApiClient {
|
|||
return AssetMetadataUpsertItemDto.fromJson(value);
|
||||
case 'AssetOrder':
|
||||
return AssetOrderTypeTransformer().decode(value);
|
||||
case 'AssetOrderBy':
|
||||
return AssetOrderByTypeTransformer().decode(value);
|
||||
case 'AssetResponseDto':
|
||||
return AssetResponseDto.fromJson(value);
|
||||
case 'AssetStackResponseDto':
|
||||
|
|
|
|||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
|
|
@ -73,6 +73,9 @@ String parameterToString(dynamic value) {
|
|||
if (value is AssetOrder) {
|
||||
return AssetOrderTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetOrderBy) {
|
||||
return AssetOrderByTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetTypeEnum) {
|
||||
return AssetTypeEnumTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
|
|
|||
88
mobile/openapi/lib/model/asset_order_by.dart
generated
Normal file
88
mobile/openapi/lib/model/asset_order_by.dart
generated
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class AssetOrderBy {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetOrderBy._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const ADDED = AssetOrderBy._(r'DATE_ADDED');
|
||||
static const DELETED = AssetOrderBy._(r'DATE_DELETED');
|
||||
static const TAKEN = AssetOrderBy._(r'DATE_TAKEN');
|
||||
|
||||
/// List of all possible values in this [enum][AssetOrderBy].
|
||||
static const values = <AssetOrderBy>[
|
||||
ADDED,
|
||||
DELETED,
|
||||
TAKEN,
|
||||
];
|
||||
|
||||
static AssetOrderBy? fromJson(dynamic value) => AssetOrderByTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetOrderBy> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetOrderBy>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetOrderBy.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetOrderBy] to String,
|
||||
/// and [decode] dynamic data back to [AssetOrderBy].
|
||||
class AssetOrderByTypeTransformer {
|
||||
factory AssetOrderByTypeTransformer() => _instance ??= const AssetOrderByTypeTransformer._();
|
||||
|
||||
const AssetOrderByTypeTransformer._();
|
||||
|
||||
String encode(AssetOrderBy data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetOrderBy.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
AssetOrderBy? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'DATE_ADDED': return AssetOrderBy.ADDED;
|
||||
case r'DATE_DELETED': return AssetOrderBy.DELETED;
|
||||
case r'DATE_TAKEN': return AssetOrderBy.TAKEN;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetOrderByTypeTransformer] instance.
|
||||
static AssetOrderByTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
|
@ -8841,6 +8841,15 @@
|
|||
"$ref": "#/components/schemas/AssetOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orderBy",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetOrderBy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "personId",
|
||||
"required": false,
|
||||
|
|
@ -9005,6 +9014,15 @@
|
|||
"$ref": "#/components/schemas/AssetOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orderBy",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetOrderBy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "personId",
|
||||
"required": false,
|
||||
|
|
@ -11056,6 +11074,14 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AssetOrderBy": {
|
||||
"enum": [
|
||||
"DATE_ADDED",
|
||||
"DATE_DELETED",
|
||||
"DATE_TAKEN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AssetResponseDto": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
|
|
|
|||
|
|
@ -4306,12 +4306,13 @@ export function tagAssets({ id, bulkIdsDto }: {
|
|||
/**
|
||||
* This endpoint requires the `asset.read` permission.
|
||||
*/
|
||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, orderBy, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
order?: AssetOrder;
|
||||
orderBy?: AssetOrderBy;
|
||||
personId?: string;
|
||||
slug?: string;
|
||||
tagId?: string;
|
||||
|
|
@ -4331,6 +4332,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
|||
isTrashed,
|
||||
key,
|
||||
order,
|
||||
orderBy,
|
||||
personId,
|
||||
slug,
|
||||
tagId,
|
||||
|
|
@ -4347,12 +4349,13 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
|||
/**
|
||||
* This endpoint requires the `asset.read` permission.
|
||||
*/
|
||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, orderBy, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
order?: AssetOrder;
|
||||
orderBy?: AssetOrderBy;
|
||||
personId?: string;
|
||||
slug?: string;
|
||||
tagId?: string;
|
||||
|
|
@ -4371,6 +4374,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
|
|||
isTrashed,
|
||||
key,
|
||||
order,
|
||||
orderBy,
|
||||
personId,
|
||||
slug,
|
||||
tagId,
|
||||
|
|
@ -5047,3 +5051,8 @@ export enum OAuthTokenEndpointAuthMethod {
|
|||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
}
|
||||
export enum AssetOrderBy {
|
||||
DateAdded = "DATE_ADDED",
|
||||
DateDeleted = "DATE_DELETED",
|
||||
DateTaken = "DATE_TAKEN"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
import { IsString } from 'class-validator';
|
||||
import { AssetOrder, AssetVisibility } from 'src/enum';
|
||||
import { AssetOrder, AssetOrderBy, AssetVisibility } from 'src/enum';
|
||||
import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TimeBucketDto {
|
||||
|
|
@ -46,6 +46,14 @@ export class TimeBucketDto {
|
|||
})
|
||||
order?: AssetOrder;
|
||||
|
||||
@ValidateEnum({
|
||||
enum: AssetOrderBy,
|
||||
name: 'AssetOrderBy',
|
||||
description: 'The field to sort time bucket assets by (DATE_TAKEN, DATE_ADDED, DATE_DELETED)',
|
||||
optional: true,
|
||||
})
|
||||
orderBy?: AssetOrderBy;
|
||||
|
||||
@ValidateEnum({
|
||||
enum: AssetVisibility,
|
||||
name: 'AssetVisibility',
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ export enum AssetOrder {
|
|||
Desc = 'desc',
|
||||
}
|
||||
|
||||
export enum AssetOrderBy {
|
||||
DateAdded = 'DATE_ADDED',
|
||||
DateDeleted = 'DATE_DELETED',
|
||||
DateTaken = 'DATE_TAKEN',
|
||||
}
|
||||
|
||||
export enum DatabaseAction {
|
||||
Create = 'CREATE',
|
||||
Update = 'UPDATE',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { isEmpty, isUndefined, omitBy } from 'lodash';
|
|||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Stack } from 'src/database';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetMetadataKey,
|
||||
AssetOrder,
|
||||
AssetOrderBy,
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
} from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
|
|
@ -14,9 +22,11 @@ import { AssetMetadataItem } from 'src/types';
|
|||
import {
|
||||
anyUuid,
|
||||
asUuid,
|
||||
getTimeBucket,
|
||||
getTimeBucketOffset,
|
||||
getTimeBucketRef,
|
||||
hasPeople,
|
||||
removeUndefinedKeys,
|
||||
truncatedDate,
|
||||
unnest,
|
||||
withDefaultVisibility,
|
||||
withExif,
|
||||
|
|
@ -65,6 +75,7 @@ interface AssetBuilderOptions {
|
|||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
order?: AssetOrder;
|
||||
orderBy?: AssetOrderBy;
|
||||
}
|
||||
|
||||
export interface TimeBucketItem {
|
||||
|
|
@ -550,11 +561,13 @@ export class AssetRepository {
|
|||
|
||||
@GenerateSql({ params: [{}] })
|
||||
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
||||
const orderBy = options.orderBy ?? AssetOrderBy.DateTaken;
|
||||
|
||||
return this.db
|
||||
.with('asset', (qb) =>
|
||||
qb
|
||||
.selectFrom('asset')
|
||||
.select(truncatedDate<Date>().as('timeBucket'))
|
||||
.select((eb) => getTimeBucket<Date>(eb, orderBy).as('timeBucket'))
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(options.visibility === undefined, withDefaultVisibility)
|
||||
|
|
@ -581,7 +594,7 @@ export class AssetRepository {
|
|||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
||||
)
|
||||
.selectFrom('asset')
|
||||
.select(sql<string>`("timeBucket" AT TIME ZONE 'UTC')::date::text`.as('timeBucket'))
|
||||
.select('timeBucket')
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.groupBy('timeBucket')
|
||||
.orderBy('timeBucket', options.order ?? 'desc')
|
||||
|
|
@ -592,6 +605,8 @@ export class AssetRepository {
|
|||
params: [DummyValue.TIME_BUCKET, { withStacked: true }],
|
||||
})
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
|
||||
const orderBy = options.orderBy ?? AssetOrderBy.DateTaken;
|
||||
|
||||
const query = this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
|
|
@ -605,12 +620,10 @@ export class AssetRepository {
|
|||
sql`asset.type = 'IMAGE'`.as('isImage'),
|
||||
sql`asset."deletedAt" is not null`.as('isTrashed'),
|
||||
'asset.livePhotoVideoId',
|
||||
sql`extract(epoch from (asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as(
|
||||
'localOffsetHours',
|
||||
),
|
||||
getTimeBucketOffset(eb, orderBy).as('localOffsetHours'),
|
||||
'asset.ownerId',
|
||||
'asset.status',
|
||||
sql`asset."fileCreatedAt" at time zone 'utc'`.as('fileCreatedAt'),
|
||||
sql`${getTimeBucketRef(eb, orderBy)} at time zone 'utc'`.as('fileCreatedAt'),
|
||||
eb.fn('encode', ['asset.thumbhash', sql.lit('base64')]).as('thumbhash'),
|
||||
'asset_exif.city',
|
||||
'asset_exif.country',
|
||||
|
|
@ -633,7 +646,7 @@ export class AssetRepository {
|
|||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(options.visibility == undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
|
||||
.where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.where((eb) => eb(getTimeBucket<string>(eb, orderBy), '=', timeBucket.replace(/^[+-]/, '')))
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.exists(
|
||||
|
|
@ -679,7 +692,7 @@ export class AssetRepository {
|
|||
)
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.orderBy('asset.fileCreatedAt', options.order ?? 'desc'),
|
||||
.orderBy((eb) => getTimeBucketRef(eb, orderBy), options.order ?? 'desc'),
|
||||
)
|
||||
.with('agg', (qb) =>
|
||||
qb
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|||
import { parse } from 'pg-connection-string';
|
||||
import postgres, { Notice, PostgresError } from 'postgres';
|
||||
import { columns, Exif, Person } from 'src/database';
|
||||
import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
|
||||
import { AssetFileType, AssetOrderBy, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
||||
|
|
@ -282,8 +282,30 @@ export function withTags(eb: ExpressionBuilder<DB, 'asset'>) {
|
|||
).as('tags');
|
||||
}
|
||||
|
||||
export function truncatedDate<O>() {
|
||||
return sql<O>`date_trunc(${sql.lit('MONTH')}, "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||
export function getTimeBucketRef(eb: ExpressionBuilder<DB, 'asset'>, orderBy: AssetOrderBy) {
|
||||
if (orderBy === AssetOrderBy.DateAdded) {
|
||||
return eb.ref('asset.createdAt');
|
||||
} else if (orderBy === AssetOrderBy.DateDeleted) {
|
||||
return eb.ref('asset.deletedAt');
|
||||
}
|
||||
|
||||
return eb.ref('asset.localDateTime');
|
||||
}
|
||||
|
||||
export function getTimeBucketOffset(eb: ExpressionBuilder<DB, 'asset'>, orderBy: AssetOrderBy) {
|
||||
if (orderBy === AssetOrderBy.DateTaken) {
|
||||
return sql`extract(epoch from (asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`;
|
||||
}
|
||||
|
||||
return sql.lit(0);
|
||||
}
|
||||
|
||||
export function getTimeBucket<O>(
|
||||
eb: ExpressionBuilder<DB, 'asset'>,
|
||||
orderBy: AssetOrderBy,
|
||||
timezone: Expression<string> = sql.lit('UTC'),
|
||||
) {
|
||||
return sql<O>`date_trunc(${sql.lit('MONTH')}, ${getTimeBucketRef(eb, orderBy)}, ${timezone})`;
|
||||
}
|
||||
|
||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
mdiFolderOutline,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiClock,
|
||||
mdiClockOutline,
|
||||
mdiImageAlbum,
|
||||
mdiImageMultiple,
|
||||
mdiImageMultipleOutline,
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
let isMapSelected: boolean = $state(false);
|
||||
let isPeopleSelected: boolean = $state(false);
|
||||
let isPhotosSelected: boolean = $state(false);
|
||||
let isRecentlyAddedSelected: boolean = $state(false);
|
||||
let isSharingSelected: boolean = $state(false);
|
||||
let isTrashSelected: boolean = $state(false);
|
||||
let isUtilitiesSelected: boolean = $state(false);
|
||||
|
|
@ -118,6 +121,13 @@
|
|||
<SideBarLink title={$t('folders')} href={resolve('/(user)/folders')} icon={mdiFolderOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('recently_added_page_title')}
|
||||
href={resolve('/(user)/recently-added')}
|
||||
bind:isSelected={isRecentlyAddedSelected}
|
||||
icon={isRecentlyAddedSelected ? mdiClock : mdiClockOutline}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('utilities')}
|
||||
href={resolve('/(user)/utilities')}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import AssetJobActions from '$lib/components/timeline/actions/AssetJobActions.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||
import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte';
|
||||
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
|
||||
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
|
||||
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { AssetOrderBy, AssetVisibility } from '@immich/sdk';
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({
|
||||
visibility: AssetVisibility.Timeline,
|
||||
withStacked: true,
|
||||
withPartners: true,
|
||||
orderBy: AssetOrderBy.DateAdded,
|
||||
});
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEscape = () => {
|
||||
if (assetInteraction.selectionActive) {
|
||||
assetInteraction.clearMultiselect();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetVisibility = (assetIds: string[]) => {
|
||||
timelineManager.removeAssets(assetIds);
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
{timelineManager}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.UNARCHIVE}
|
||||
onEscape={handleEscape}
|
||||
>
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_archived_assets_message')} />
|
||||
{/snippet}
|
||||
</Timeline>
|
||||
</UserPageLayout>
|
||||
|
||||
{#if assetInteraction.selectionActive}
|
||||
<AssetSelectControlBar
|
||||
ownerId={$user.id}
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||
>
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets {timelineManager} {assetInteraction} />
|
||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) =>
|
||||
timelineManager.updateAssetOperation(ids, (asset) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
return { remove: false };
|
||||
})}
|
||||
></FavoriteAction>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem onArchive={(assetIds) => timelineManager.removeAssets(assetIds)} />
|
||||
{#if $preferences.tags.enabled}
|
||||
<TagAction menuItem />
|
||||
{/if}
|
||||
<DeleteAssets
|
||||
menuItem
|
||||
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
|
||||
onUndoDelete={(assets) => timelineManager.addAssets(assets)}
|
||||
/>
|
||||
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, url }) => {
|
||||
await authenticate(url);
|
||||
const asset = await getAssetInfoFromParam(params);
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
asset,
|
||||
meta: {
|
||||
title: $t('recently_added'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { emptyTrash, restoreTrash } from '@immich/sdk';
|
||||
import { AssetOrderBy, emptyTrash, restoreTrash } from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ isTrashed: true });
|
||||
void timelineManager.updateOptions({ isTrashed: true, orderBy: AssetOrderBy.DateDeleted });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue