Adapt web client to consume new server response format

This commit is contained in:
Min Idzelis 2025-04-29 13:45:40 +00:00
parent 077703adcc
commit bc5d4b45a6
17 changed files with 367 additions and 60 deletions

View File

@ -145,8 +145,15 @@ Class | Method | HTTP request | Description
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | *MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories | *MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} | *MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} | *NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} |
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email | *NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications |
*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} |
*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications |
*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} |
*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications |
*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications |
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} |
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email |
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
@ -300,7 +307,6 @@ Class | Method | HTTP request | Description
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md) - [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md) - [AudioCodec](doc//AudioCodec.md)
- [AvatarResponse](doc//AvatarResponse.md)
- [AvatarUpdate](doc//AvatarUpdate.md) - [AvatarUpdate](doc//AvatarUpdate.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md) - [BulkIdsDto](doc//BulkIdsDto.md)
@ -361,6 +367,13 @@ Class | Method | HTTP request | Description
- [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md)
- [MergePersonDto](doc//MergePersonDto.md) - [MergePersonDto](doc//MergePersonDto.md)
- [MetadataSearchDto](doc//MetadataSearchDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md)
- [NotificationCreateDto](doc//NotificationCreateDto.md)
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
- [NotificationDto](doc//NotificationDto.md)
- [NotificationLevel](doc//NotificationLevel.md)
- [NotificationType](doc//NotificationType.md)
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
- [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md)
@ -475,8 +488,12 @@ Class | Method | HTTP request | Description
- [TemplateDto](doc//TemplateDto.md) - [TemplateDto](doc//TemplateDto.md)
- [TemplateResponseDto](doc//TemplateResponseDto.md) - [TemplateResponseDto](doc//TemplateResponseDto.md)
- [TestEmailResponseDto](doc//TestEmailResponseDto.md) - [TestEmailResponseDto](doc//TestEmailResponseDto.md)
- [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md)
- [TimeBucketAssetResponseDtoDurationInner](doc//TimeBucketAssetResponseDtoDurationInner.md)
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
- [TimeBucketSize](doc//TimeBucketSize.md) - [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md)
- [TimelineAssetDescriptionDto](doc//TimelineAssetDescriptionDto.md)
- [TimelineStackResponseDto](doc//TimelineStackResponseDto.md)
- [ToneMapping](doc//ToneMapping.md) - [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md) - [TranscodePolicy](doc//TranscodePolicy.md)

View File

@ -44,6 +44,7 @@ part 'api/jobs_api.dart';
part 'api/libraries_api.dart'; part 'api/libraries_api.dart';
part 'api/map_api.dart'; part 'api/map_api.dart';
part 'api/memories_api.dart'; part 'api/memories_api.dart';
part 'api/notifications_api.dart';
part 'api/notifications_admin_api.dart'; part 'api/notifications_admin_api.dart';
part 'api/o_auth_api.dart'; part 'api/o_auth_api.dart';
part 'api/partners_api.dart'; part 'api/partners_api.dart';
@ -107,7 +108,6 @@ part 'model/asset_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart'; part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart'; part 'model/asset_type_enum.dart';
part 'model/audio_codec.dart'; part 'model/audio_codec.dart';
part 'model/avatar_response.dart';
part 'model/avatar_update.dart'; part 'model/avatar_update.dart';
part 'model/bulk_id_response_dto.dart'; part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart'; part 'model/bulk_ids_dto.dart';
@ -168,6 +168,13 @@ part 'model/memory_type.dart';
part 'model/memory_update_dto.dart'; part 'model/memory_update_dto.dart';
part 'model/merge_person_dto.dart'; part 'model/merge_person_dto.dart';
part 'model/metadata_search_dto.dart'; part 'model/metadata_search_dto.dart';
part 'model/notification_create_dto.dart';
part 'model/notification_delete_all_dto.dart';
part 'model/notification_dto.dart';
part 'model/notification_level.dart';
part 'model/notification_type.dart';
part 'model/notification_update_all_dto.dart';
part 'model/notification_update_dto.dart';
part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_dto.dart';
@ -282,8 +289,12 @@ part 'model/tags_update.dart';
part 'model/template_dto.dart'; part 'model/template_dto.dart';
part 'model/template_response_dto.dart'; part 'model/template_response_dto.dart';
part 'model/test_email_response_dto.dart'; part 'model/test_email_response_dto.dart';
part 'model/time_bucket_asset_response_dto.dart';
part 'model/time_bucket_asset_response_dto_duration_inner.dart';
part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_response_dto.dart';
part 'model/time_bucket_size.dart'; part 'model/time_buckets_response_dto.dart';
part 'model/timeline_asset_description_dto.dart';
part 'model/timeline_stack_response_dto.dart';
part 'model/tone_mapping.dart'; part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart'; part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart'; part 'model/transcode_policy.dart';

View File

@ -270,8 +270,6 @@ class ApiClient {
return AssetTypeEnumTypeTransformer().decode(value); return AssetTypeEnumTypeTransformer().decode(value);
case 'AudioCodec': case 'AudioCodec':
return AudioCodecTypeTransformer().decode(value); return AudioCodecTypeTransformer().decode(value);
case 'AvatarResponse':
return AvatarResponse.fromJson(value);
case 'AvatarUpdate': case 'AvatarUpdate':
return AvatarUpdate.fromJson(value); return AvatarUpdate.fromJson(value);
case 'BulkIdResponseDto': case 'BulkIdResponseDto':
@ -392,6 +390,20 @@ class ApiClient {
return MergePersonDto.fromJson(value); return MergePersonDto.fromJson(value);
case 'MetadataSearchDto': case 'MetadataSearchDto':
return MetadataSearchDto.fromJson(value); return MetadataSearchDto.fromJson(value);
case 'NotificationCreateDto':
return NotificationCreateDto.fromJson(value);
case 'NotificationDeleteAllDto':
return NotificationDeleteAllDto.fromJson(value);
case 'NotificationDto':
return NotificationDto.fromJson(value);
case 'NotificationLevel':
return NotificationLevelTypeTransformer().decode(value);
case 'NotificationType':
return NotificationTypeTypeTransformer().decode(value);
case 'NotificationUpdateAllDto':
return NotificationUpdateAllDto.fromJson(value);
case 'NotificationUpdateDto':
return NotificationUpdateDto.fromJson(value);
case 'OAuthAuthorizeResponseDto': case 'OAuthAuthorizeResponseDto':
return OAuthAuthorizeResponseDto.fromJson(value); return OAuthAuthorizeResponseDto.fromJson(value);
case 'OAuthCallbackDto': case 'OAuthCallbackDto':
@ -620,10 +632,18 @@ class ApiClient {
return TemplateResponseDto.fromJson(value); return TemplateResponseDto.fromJson(value);
case 'TestEmailResponseDto': case 'TestEmailResponseDto':
return TestEmailResponseDto.fromJson(value); return TestEmailResponseDto.fromJson(value);
case 'TimeBucketAssetResponseDto':
return TimeBucketAssetResponseDto.fromJson(value);
case 'TimeBucketAssetResponseDtoDurationInner':
return TimeBucketAssetResponseDtoDurationInner.fromJson(value);
case 'TimeBucketResponseDto': case 'TimeBucketResponseDto':
return TimeBucketResponseDto.fromJson(value); return TimeBucketResponseDto.fromJson(value);
case 'TimeBucketSize': case 'TimeBucketsResponseDto':
return TimeBucketSizeTypeTransformer().decode(value); return TimeBucketsResponseDto.fromJson(value);
case 'TimelineAssetDescriptionDto':
return TimelineAssetDescriptionDto.fromJson(value);
case 'TimelineStackResponseDto':
return TimelineStackResponseDto.fromJson(value);
case 'ToneMapping': case 'ToneMapping':
return ToneMappingTypeTransformer().decode(value); return ToneMappingTypeTransformer().decode(value);
case 'TranscodeHWAccel': case 'TranscodeHWAccel':

View File

@ -100,6 +100,12 @@ String parameterToString(dynamic value) {
if (value is MemoryType) { if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString(); return MemoryTypeTypeTransformer().encode(value).toString();
} }
if (value is NotificationLevel) {
return NotificationLevelTypeTransformer().encode(value).toString();
}
if (value is NotificationType) {
return NotificationTypeTypeTransformer().encode(value).toString();
}
if (value is PartnerDirection) { if (value is PartnerDirection) {
return PartnerDirectionTypeTransformer().encode(value).toString(); return PartnerDirectionTypeTransformer().encode(value).toString();
} }
@ -133,9 +139,6 @@ String parameterToString(dynamic value) {
if (value is SyncRequestType) { if (value is SyncRequestType) {
return SyncRequestTypeTypeTransformer().encode(value).toString(); return SyncRequestTypeTypeTransformer().encode(value).toString();
} }
if (value is TimeBucketSize) {
return TimeBucketSizeTypeTransformer().encode(value).toString();
}
if (value is ToneMapping) { if (value is ToneMapping) {
return ToneMappingTypeTransformer().encode(value).toString(); return ToneMappingTypeTransformer().encode(value).toString();
} }

View File

@ -13,6 +13,7 @@ part of openapi.api;
class TimeBucketAssetResponseDto { class TimeBucketAssetResponseDto {
/// Returns a new [TimeBucketAssetResponseDto] instance. /// Returns a new [TimeBucketAssetResponseDto] instance.
TimeBucketAssetResponseDto({ TimeBucketAssetResponseDto({
this.description = const [],
this.duration = const [], this.duration = const [],
this.id = const [], this.id = const [],
this.isArchived = const [], this.isArchived = const [],
@ -29,6 +30,8 @@ class TimeBucketAssetResponseDto {
this.thumbhash = const [], this.thumbhash = const [],
}); });
List<TimelineAssetDescriptionDto> description;
List<TimeBucketAssetResponseDtoDurationInner> duration; List<TimeBucketAssetResponseDtoDurationInner> duration;
List<String> id; List<String> id;
@ -59,6 +62,7 @@ class TimeBucketAssetResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto &&
_deepEquality.equals(other.description, description) &&
_deepEquality.equals(other.duration, duration) && _deepEquality.equals(other.duration, duration) &&
_deepEquality.equals(other.id, id) && _deepEquality.equals(other.id, id) &&
_deepEquality.equals(other.isArchived, isArchived) && _deepEquality.equals(other.isArchived, isArchived) &&
@ -77,6 +81,7 @@ class TimeBucketAssetResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(description.hashCode) +
(duration.hashCode) + (duration.hashCode) +
(id.hashCode) + (id.hashCode) +
(isArchived.hashCode) + (isArchived.hashCode) +
@ -93,10 +98,11 @@ class TimeBucketAssetResponseDto {
(thumbhash.hashCode); (thumbhash.hashCode);
@override @override
String toString() => 'TimeBucketAssetResponseDto[duration=$duration, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, isVideo=$isVideo, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash]'; String toString() => 'TimeBucketAssetResponseDto[description=$description, duration=$duration, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, isVideo=$isVideo, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'description'] = this.description;
json[r'duration'] = this.duration; json[r'duration'] = this.duration;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isArchived'] = this.isArchived; json[r'isArchived'] = this.isArchived;
@ -123,6 +129,7 @@ class TimeBucketAssetResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return TimeBucketAssetResponseDto( return TimeBucketAssetResponseDto(
description: TimelineAssetDescriptionDto.listFromJson(json[r'description']),
duration: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'duration']), duration: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'duration']),
id: json[r'id'] is Iterable id: json[r'id'] is Iterable
? (json[r'id'] as Iterable).cast<String>().toList(growable: false) ? (json[r'id'] as Iterable).cast<String>().toList(growable: false)
@ -200,6 +207,7 @@ class TimeBucketAssetResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'description',
'duration', 'duration',
'id', 'id',
'isArchived', 'isArchived',

View File

@ -0,0 +1,115 @@
//
// 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 TimelineAssetDescriptionDto {
/// Returns a new [TimelineAssetDescriptionDto] instance.
TimelineAssetDescriptionDto({
required this.city,
required this.country,
});
String? city;
String? country;
@override
bool operator ==(Object other) => identical(this, other) || other is TimelineAssetDescriptionDto &&
other.city == city &&
other.country == country;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode);
@override
String toString() => 'TimelineAssetDescriptionDto[city=$city, country=$country]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.city != null) {
json[r'city'] = this.city;
} else {
// json[r'city'] = null;
}
if (this.country != null) {
json[r'country'] = this.country;
} else {
// json[r'country'] = null;
}
return json;
}
/// Returns a new [TimelineAssetDescriptionDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TimelineAssetDescriptionDto? fromJson(dynamic value) {
upgradeDto(value, "TimelineAssetDescriptionDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TimelineAssetDescriptionDto(
city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'),
);
}
return null;
}
static List<TimelineAssetDescriptionDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimelineAssetDescriptionDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimelineAssetDescriptionDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TimelineAssetDescriptionDto> mapFromJson(dynamic json) {
final map = <String, TimelineAssetDescriptionDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TimelineAssetDescriptionDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TimelineAssetDescriptionDto-objects as value to a dart map
static Map<String, List<TimelineAssetDescriptionDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimelineAssetDescriptionDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TimelineAssetDescriptionDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'city',
'country',
};
}

View File

@ -13847,6 +13847,13 @@
}, },
"TimeBucketAssetResponseDto": { "TimeBucketAssetResponseDto": {
"properties": { "properties": {
"description": {
"default": [],
"items": {
"$ref": "#/components/schemas/TimelineAssetDescriptionDto"
},
"type": "array"
},
"duration": { "duration": {
"default": [], "default": [],
"items": { "items": {
@ -13976,6 +13983,7 @@
} }
}, },
"required": [ "required": [
"description",
"duration", "duration",
"id", "id",
"isArchived", "isArchived",
@ -14023,6 +14031,23 @@
], ],
"type": "object" "type": "object"
}, },
"TimelineAssetDescriptionDto": {
"properties": {
"city": {
"nullable": true,
"type": "string"
},
"country": {
"nullable": true,
"type": "string"
}
},
"required": [
"city",
"country"
],
"type": "object"
},
"TimelineStackResponseDto": { "TimelineStackResponseDto": {
"properties": { "properties": {
"assetCount": { "assetCount": {

View File

@ -1407,12 +1407,17 @@ export type TagBulkAssetsResponseDto = {
export type TagUpdateDto = { export type TagUpdateDto = {
color?: string | null; color?: string | null;
}; };
export type TimelineAssetDescriptionDto = {
city: string | null;
country: string | null;
};
export type TimelineStackResponseDto = { export type TimelineStackResponseDto = {
assetCount: number; assetCount: number;
id: string; id: string;
primaryAssetId: string; primaryAssetId: string;
}; };
export type TimeBucketAssetResponseDto = { export type TimeBucketAssetResponseDto = {
description: TimelineAssetDescriptionDto[];
duration: (string | number)[]; duration: (string | number)[];
id: string[]; id: string[];
isArchived: number[]; isArchived: number[];

View File

@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsString, Min } from 'class-validator'; import { IsEnum, IsInt, IsString, Min } from 'class-validator';
import { AssetOrder } from 'src/enum'; import { AssetOrder } from 'src/enum';
import { TimeBucketAssets, TimelineStack } from 'src/services/timeline.service.types'; import { AssetDescription, TimeBucketAssets, TimelineStack } from 'src/services/timeline.service.types';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class TimeBucketDto { export class TimeBucketDto {
@ -64,6 +64,13 @@ export class TimelineStackResponseDto implements TimelineStack {
assetCount!: number; assetCount!: number;
} }
export class TimelineAssetDescriptionDto implements AssetDescription {
@ApiProperty()
city!: string | null;
@ApiProperty()
country!: string | null;
}
export class TimeBucketAssetResponseDto implements TimeBucketAssets { export class TimeBucketAssetResponseDto implements TimeBucketAssets {
@ApiProperty({ type: [String] }) @ApiProperty({ type: [String] })
id: string[] = []; id: string[] = [];
@ -154,6 +161,9 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets {
}, },
}) })
livePhotoVideoId: (string | number)[] = []; livePhotoVideoId: (string | number)[] = [];
@ApiProperty()
description: TimelineAssetDescriptionDto[] = [];
} }
export class TimeBucketsResponseDto { export class TimeBucketsResponseDto {

View File

@ -701,7 +701,14 @@ export class AssetRepository {
'livePhotoVideoId', 'livePhotoVideoId',
]) ])
.leftJoin('exif', 'assets.id', 'exif.assetId') .leftJoin('exif', 'assets.id', 'exif.assetId')
.select(['exif.exifImageHeight as height', 'exifImageWidth as width', 'exif.orientation', 'exif.projectionType']) .select([
'exif.exifImageHeight as height',
'exifImageWidth as width',
'exif.orientation',
'exif.projectionType',
'exif.city as city',
'exif.country as country',
])
.$if(!!options.albumId, (qb) => .$if(!!options.albumId, (qb) =>
qb qb
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')

View File

@ -58,6 +58,7 @@ export class TimelineService extends BaseService {
duration: [], duration: [],
projectionType: [], projectionType: [],
livePhotoVideoId: [], livePhotoVideoId: [],
description: [],
}; };
for (const item of items) { for (const item of items) {
let width = item.width!; let width = item.width!;
@ -82,6 +83,10 @@ export class TimelineService extends BaseService {
bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId || 0); bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId || 0);
bucketAssets.isImage.push(item.type === AssetType.IMAGE ? 1 : 0); bucketAssets.isImage.push(item.type === AssetType.IMAGE ? 1 : 0);
bucketAssets.isVideo.push(item.type === AssetType.VIDEO ? 1 : 0); bucketAssets.isVideo.push(item.type === AssetType.VIDEO ? 1 : 0);
bucketAssets.description.push({
city: item.city,
country: item.country,
});
} }
return { return {

View File

@ -4,6 +4,11 @@ export type TimelineStack = {
assetCount: number; assetCount: number;
}; };
export type AssetDescription = {
city: string | null;
country: string | null;
};
export type TimeBucketAssets = { export type TimeBucketAssets = {
id: string[]; id: string[];
ownerId: string[]; ownerId: string[];
@ -19,4 +24,5 @@ export type TimeBucketAssets = {
duration: (string | number)[]; duration: (string | number)[];
projectionType: (string | number)[]; projectionType: (string | number)[];
livePhotoVideoId: (string | number)[]; livePhotoVideoId: (string | number)[];
description: AssetDescription[];
}; };

View File

@ -1,8 +1,8 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { AbortError } from '$lib/utils'; import { AbortError } from '$lib/utils';
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; import { type AssetResponseDto, type TimeBucketResponseDto } from '@immich/sdk';
import { assetFactory, timelineAssetFactory } from '@test-data/factories/asset-factory'; import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
import { AssetStore } from './assets-store.svelte'; import { AssetStore, type TimelineAsset } from './assets-store.svelte';
describe('AssetStore', () => { describe('AssetStore', () => {
beforeEach(() => { beforeEach(() => {
@ -11,18 +11,22 @@ describe('AssetStore', () => {
describe('init', () => { describe('init', () => {
let assetStore: AssetStore; let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = { const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': assetFactory '2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1) .buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), .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) .buildList(100)
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), .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) .buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), .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 () => { beforeEach(async () => {
assetStore = new AssetStore(); assetStore = new AssetStore();
sdkMock.getTimeBuckets.mockResolvedValue([ sdkMock.getTimeBuckets.mockResolvedValue([
@ -30,13 +34,14 @@ describe('AssetStore', () => {
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' }, { count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-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 }); await assetStore.updateViewport({ width: 1588, height: 1000 });
}); });
it('should load buckets in viewport', () => { it('should load buckets in viewport', () => {
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1); expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month });
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2); expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
}); });
@ -48,29 +53,31 @@ describe('AssetStore', () => {
expect(plainBuckets).toEqual( expect(plainBuckets).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 304 }), expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 186.5 }),
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4515.333_333_333_333 }), expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 12_017 }),
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
]), ]),
); );
}); });
it('calculates timeline height', () => { it('calculates timeline height', () => {
expect(assetStore.timelineHeight).toBe(5105.333_333_333_333); expect(assetStore.timelineHeight).toBe(12_489.5);
}); });
}); });
describe('loadBucket', () => { describe('loadBucket', () => {
let assetStore: AssetStore; let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = { const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-01-03T00:00:00.000Z': assetFactory '2024-01-03T00:00:00.000Z': timelineAssetFactory
.buildList(1) .buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), .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) .buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), .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 () => { beforeEach(async () => {
assetStore = new AssetStore(); assetStore = new AssetStore();
sdkMock.getTimeBuckets.mockResolvedValue([ sdkMock.getTimeBuckets.mockResolvedValue([
@ -82,7 +89,7 @@ describe('AssetStore', () => {
if (signal?.aborted) { if (signal?.aborted) {
throw new AbortError(); throw new AbortError();
} }
return bucketAssets[timeBucket]; return bucketAssetsResponse[timeBucket];
}); });
await assetStore.updateViewport({ width: 1588, height: 0 }); await assetStore.updateViewport({ width: 1588, height: 0 });
}); });
@ -296,7 +303,9 @@ describe('AssetStore', () => {
}); });
it('removes asset from bucket', () => { 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.addAssets([assetOne, assetTwo]);
assetStore.removeAssets([assetOne.id]); assetStore.removeAssets([assetOne.id]);
@ -342,17 +351,20 @@ describe('AssetStore', () => {
describe('getPreviousAsset', () => { describe('getPreviousAsset', () => {
let assetStore: AssetStore; let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = { const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': assetFactory '2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1) .buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), .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) .buildList(6)
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), .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) .buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), .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 () => { beforeEach(async () => {
assetStore = new AssetStore(); assetStore = new AssetStore();
@ -361,8 +373,7 @@ describe('AssetStore', () => {
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' }, { count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-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 }); await assetStore.updateViewport({ width: 1588, height: 1000 });
}); });

View File

@ -14,9 +14,8 @@ import {
getAssetInfo, getAssetInfo,
getTimeBucket, getTimeBucket,
getTimeBuckets, getTimeBuckets,
TimeBucketSize,
type AssetResponseDto,
type AssetStackResponseDto, type AssetStackResponseDto,
type TimeBucketResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { clamp, debounce, isEqual, throttle } from 'lodash-es'; import { clamp, debounce, isEqual, throttle } from 'lodash-es';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -84,7 +83,7 @@ export type TimelineAsset = {
duration: string | null; duration: string | null;
projectionType: string | null; projectionType: string | null;
livePhotoVideoId: string | null; livePhotoVideoId: string | null;
text: { description: {
city: string | null; city: string | null;
country: string | null; country: string | null;
people: string[]; 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 // 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(); const addContext = new AddContext();
for (const asset of bucketResponse) { for (let i = 0; i < bucketResponse.bucketAssets.id.length; i++) {
const timelineAsset = toTimelineAsset(asset); 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); this.addTimelineAsset(timelineAsset, addContext);
} }
@ -878,7 +900,6 @@ export class AssetStore {
async #initialiazeTimeBuckets() { async #initialiazeTimeBuckets() {
const timebuckets = await getTimeBuckets({ const timebuckets = await getTimeBuckets({
...this.#options, ...this.#options,
size: TimeBucketSize.Month,
key: authManager.key, key: authManager.key,
}); });
@ -1086,7 +1107,7 @@ export class AssetStore {
{ {
...this.#options, ...this.#options,
timeBucket: bucketDate, timeBucket: bucketDate,
size: TimeBucketSize.Month,
key: authManager.key, key: authManager.key,
}, },
{ signal }, { signal },
@ -1097,12 +1118,11 @@ export class AssetStore {
{ {
albumId: this.#options.timelineAlbumId, albumId: this.#options.timelineAlbumId,
timeBucket: bucketDate, timeBucket: bucketDate,
size: TimeBucketSize.Month,
key: authManager.key, key: authManager.key,
}, },
{ signal }, { signal },
); );
for (const { id } of albumAssets) { for (const id of albumAssets.bucketAssets.id) {
this.albumAssets.add(id); this.albumAssets.add(id);
} }
} }

View File

@ -38,15 +38,10 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
return 300; return 300;
} }
export const getAltTextForTimelineAsset = (_: TimelineAsset) => {
// TODO: implement this in a performant way
return '';
};
export const getAltText = derived(t, ($t) => { export const getAltText = derived(t, ($t) => {
return (asset: TimelineAsset) => { return (asset: TimelineAsset) => {
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) }); 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 hasPlace = city && country;
const peopleCount = names.length; const peopleCount = names.length;

View File

@ -71,7 +71,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
const city = assetResponse.exifInfo?.city; const city = assetResponse.exifInfo?.city;
const country = assetResponse.exifInfo?.country; const country = assetResponse.exifInfo?.country;
const people = assetResponse.people?.map((person) => person.name) || []; const people = assetResponse.people?.map((person) => person.name) || [];
const text = {
const description = {
city: city || null, city: city || null,
country: country || null, country: country || null,
people, people,
@ -91,7 +92,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
duration: assetResponse.duration || null, duration: assetResponse.duration || null,
projectionType: assetResponse.exifInfo?.projectionType || null, projectionType: assetResponse.exifInfo?.projectionType || null,
livePhotoVideoId: assetResponse.livePhotoVideoId || null, livePhotoVideoId: assetResponse.livePhotoVideoId || null,
text, description,
}; };
}; };
export const isTimelineAsset = (arg: AssetResponseDto | TimelineAsset): arg is TimelineAsset => export const isTimelineAsset = (arg: AssetResponseDto | TimelineAsset): arg is TimelineAsset =>

View File

@ -1,6 +1,12 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { faker } from '@faker-js/faker'; 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'; import { Sync } from 'factory.ts';
export const assetFactory = Sync.makeFactory<AssetResponseDto>({ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
@ -42,9 +48,51 @@ export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
stack: null, stack: null,
projectionType: null, projectionType: null,
livePhotoVideoId: Sync.each(() => faker.string.uuid()), livePhotoVideoId: Sync.each(() => faker.string.uuid()),
text: Sync.each(() => ({ description: Sync.each(() => ({
city: faker.location.city(), city: faker.location.city(),
country: faker.location.country(), country: faker.location.country(),
people: [faker.person.fullName()], 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;
};