diff --git a/mobile/openapi/doc/GetAssetByTimeBucketDto.md b/mobile/openapi/doc/GetAssetByTimeBucketDto.md index b0f7212293fa3..da314c71fc726 100644 --- a/mobile/openapi/doc/GetAssetByTimeBucketDto.md +++ b/mobile/openapi/doc/GetAssetByTimeBucketDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **timeBucket** | **List** | | [default to const []] **userId** | **String** | | [optional] +**withoutThumbs** | **bool** | Include assets without thumbnails | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart b/mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart index 10210c2e6b8f0..8301132ac5da8 100644 --- a/mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart +++ b/mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart @@ -15,6 +15,7 @@ class GetAssetByTimeBucketDto { GetAssetByTimeBucketDto({ this.timeBucket = const [], this.userId, + this.withoutThumbs, }); List timeBucket; @@ -27,19 +28,30 @@ class GetAssetByTimeBucketDto { /// String? userId; + /// Include assets without thumbnails + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withoutThumbs; + @override bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto && other.timeBucket == timeBucket && - other.userId == userId; + other.userId == userId && + other.withoutThumbs == withoutThumbs; @override int get hashCode => // ignore: unnecessary_parenthesis (timeBucket.hashCode) + - (userId == null ? 0 : userId!.hashCode); + (userId == null ? 0 : userId!.hashCode) + + (withoutThumbs == null ? 0 : withoutThumbs!.hashCode); @override - String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId]'; + String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId, withoutThumbs=$withoutThumbs]'; Map toJson() { final json = {}; @@ -49,6 +61,11 @@ class GetAssetByTimeBucketDto { } else { // json[r'userId'] = null; } + if (this.withoutThumbs != null) { + json[r'withoutThumbs'] = this.withoutThumbs; + } else { + // json[r'withoutThumbs'] = null; + } return json; } @@ -75,6 +92,7 @@ class GetAssetByTimeBucketDto { ? (json[r'timeBucket'] as Iterable).cast().toList(growable: false) : const [], userId: mapValueOfType(json, r'userId'), + withoutThumbs: mapValueOfType(json, r'withoutThumbs'), ); } return null; diff --git a/mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart b/mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart index 591f461497b45..e6021df73c9f2 100644 --- a/mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart +++ b/mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart @@ -26,6 +26,12 @@ void main() { // TODO }); + // Include assets without thumbnails + // bool withoutThumbs + test('to test the property `withoutThumbs`', () async { + // TODO + }); + }); diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index cf033c783a257..982e5c9f536fc 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -104,19 +104,23 @@ export class AssetRepository implements IAssetRepository { return this.getAssetCount(items); } - async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise { + async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise { // Get asset entity from a list of time buckets - return await this.assetRepository + let builder = this.assetRepository .createQueryBuilder('asset') .where('asset.ownerId = :userId', { userId: userId }) .andWhere(`date_trunc('month', "fileCreatedAt") IN (:...buckets)`, { - buckets: [...getAssetByTimeBucketDto.timeBucket], + buckets: [...dto.timeBucket], }) - .andWhere('asset.resizePath is not NULL') .andWhere('asset.isVisible = true') .andWhere('asset.isArchived = false') - .orderBy('asset.fileCreatedAt', 'DESC') - .getMany(); + .orderBy('asset.fileCreatedAt', 'DESC'); + + if (!dto.withoutThumbs) { + builder = builder.andWhere('asset.resizePath is not NULL'); + } + + return builder.getMany(); } async getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum) { diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts index 6203c3e04ed4a..ad846751c6295 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { toBoolean } from '../../../utils/transform.util'; export class GetAssetByTimeBucketDto { @IsNotEmpty() @@ -15,4 +17,12 @@ export class GetAssetByTimeBucketDto { @IsUUID('4') @ApiProperty({ format: 'uuid' }) userId?: string; + + /** + * Include assets without thumbnails + */ + @IsOptional() + @IsBoolean() + @Transform(toBoolean) + withoutThumbs?: boolean; } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 10704b9bd3f7b..15537730c4a4c 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5954,6 +5954,10 @@ "userId": { "type": "string", "format": "uuid" + }, + "withoutThumbs": { + "type": "boolean", + "description": "Include assets without thumbnails" } }, "required": [ diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 37d1ce449145a..2a8a9cee2c494 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1324,6 +1324,12 @@ export interface GetAssetByTimeBucketDto { * @memberof GetAssetByTimeBucketDto */ 'userId'?: string; + /** + * Include assets without thumbnails + * @type {boolean} + * @memberof GetAssetByTimeBucketDto + */ + 'withoutThumbs'?: boolean; } /** * diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4c827fca255de..4f61239f63340 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -11,6 +11,7 @@ import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ChevronRight from 'svelte-material-icons/ChevronRight.svelte'; + import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte'; import { fly } from 'svelte/transition'; import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte'; import { @@ -350,7 +351,15 @@
{#key asset.id} - {#if asset.type === AssetTypeEnum.Image} + {#if !asset.resizePath} +
+
+ +
+
+ {:else if asset.type === AssetTypeEnum.Image} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
@@ -121,12 +122,19 @@
{/if} - + + {#if asset.resizePath} + + {:else} +
+ +
+ {/if} {#if asset.type === AssetTypeEnum.Video}
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 61023c64c2db7..cf8865fc07f49 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -67,7 +67,8 @@ function createAssetStore() { const { data: assets } = await api.assetApi.getAssetByTimeBucket( { timeBucket: [bucket], - userId: _assetGridState.userId + userId: _assetGridState.userId, + withoutThumbs: true }, { signal: currentBucketData?.cancelToken.signal } );