浏览代码

feat(web): show assets without thumbs (#2561)

* feat(web): show assets without thumbnails

* chore: open api
Jason Rasmussen 2 年之前
父节点
当前提交
1613ae9185

+ 1 - 0
mobile/openapi/doc/GetAssetByTimeBucketDto.md

@@ -10,6 +10,7 @@ Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 ------------ | ------------- | ------------- | -------------
 **timeBucket** | **List<String>** |  | [default to const []]
 **timeBucket** | **List<String>** |  | [default to const []]
 **userId** | **String** |  | [optional] 
 **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)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 21 - 3
mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart

@@ -15,6 +15,7 @@ class GetAssetByTimeBucketDto {
   GetAssetByTimeBucketDto({
   GetAssetByTimeBucketDto({
     this.timeBucket = const [],
     this.timeBucket = const [],
     this.userId,
     this.userId,
+    this.withoutThumbs,
   });
   });
 
 
   List<String> timeBucket;
   List<String> timeBucket;
@@ -27,19 +28,30 @@ class GetAssetByTimeBucketDto {
   ///
   ///
   String? userId;
   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
   @override
   bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto &&
   bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto &&
      other.timeBucket == timeBucket &&
      other.timeBucket == timeBucket &&
-     other.userId == userId;
+     other.userId == userId &&
+     other.withoutThumbs == withoutThumbs;
 
 
   @override
   @override
   int get hashCode =>
   int get hashCode =>
     // ignore: unnecessary_parenthesis
     // ignore: unnecessary_parenthesis
     (timeBucket.hashCode) +
     (timeBucket.hashCode) +
-    (userId == null ? 0 : userId!.hashCode);
+    (userId == null ? 0 : userId!.hashCode) +
+    (withoutThumbs == null ? 0 : withoutThumbs!.hashCode);
 
 
   @override
   @override
-  String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId]';
+  String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId, withoutThumbs=$withoutThumbs]';
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     final json = <String, dynamic>{};
@@ -49,6 +61,11 @@ class GetAssetByTimeBucketDto {
     } else {
     } else {
       // json[r'userId'] = null;
       // json[r'userId'] = null;
     }
     }
+    if (this.withoutThumbs != null) {
+      json[r'withoutThumbs'] = this.withoutThumbs;
+    } else {
+      // json[r'withoutThumbs'] = null;
+    }
     return json;
     return json;
   }
   }
 
 
@@ -75,6 +92,7 @@ class GetAssetByTimeBucketDto {
             ? (json[r'timeBucket'] as Iterable).cast<String>().toList(growable: false)
             ? (json[r'timeBucket'] as Iterable).cast<String>().toList(growable: false)
             : const [],
             : const [],
         userId: mapValueOfType<String>(json, r'userId'),
         userId: mapValueOfType<String>(json, r'userId'),
+        withoutThumbs: mapValueOfType<bool>(json, r'withoutThumbs'),
       );
       );
     }
     }
     return null;
     return null;

+ 6 - 0
mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart

@@ -26,6 +26,12 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // Include assets without thumbnails
+    // bool withoutThumbs
+    test('to test the property `withoutThumbs`', () async {
+      // TODO
+    });
+
 
 
   });
   });
 
 

+ 10 - 6
server/apps/immich/src/api-v1/asset/asset-repository.ts

@@ -104,19 +104,23 @@ export class AssetRepository implements IAssetRepository {
     return this.getAssetCount(items);
     return this.getAssetCount(items);
   }
   }
 
 
-  async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
+  async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
     // Get asset entity from a list of time buckets
     // Get asset entity from a list of time buckets
-    return await this.assetRepository
+    let builder = this.assetRepository
       .createQueryBuilder('asset')
       .createQueryBuilder('asset')
       .where('asset.ownerId = :userId', { userId: userId })
       .where('asset.ownerId = :userId', { userId: userId })
       .andWhere(`date_trunc('month', "fileCreatedAt") IN (:...buckets)`, {
       .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.isVisible = true')
       .andWhere('asset.isArchived = false')
       .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) {
   async getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum) {

+ 11 - 1
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 { 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 {
 export class GetAssetByTimeBucketDto {
   @IsNotEmpty()
   @IsNotEmpty()
@@ -15,4 +17,12 @@ export class GetAssetByTimeBucketDto {
   @IsUUID('4')
   @IsUUID('4')
   @ApiProperty({ format: 'uuid' })
   @ApiProperty({ format: 'uuid' })
   userId?: string;
   userId?: string;
+
+  /**
+   * Include assets without thumbnails
+   */
+  @IsOptional()
+  @IsBoolean()
+  @Transform(toBoolean)
+  withoutThumbs?: boolean;
 }
 }

+ 4 - 0
server/immich-openapi-specs.json

@@ -5988,6 +5988,10 @@
           "userId": {
           "userId": {
             "type": "string",
             "type": "string",
             "format": "uuid"
             "format": "uuid"
+          },
+          "withoutThumbs": {
+            "type": "boolean",
+            "description": "Include assets without thumbnails"
           }
           }
         },
         },
         "required": [
         "required": [

+ 6 - 0
web/src/api/open-api/api.ts

@@ -1330,6 +1330,12 @@ export interface GetAssetByTimeBucketDto {
      * @memberof GetAssetByTimeBucketDto
      * @memberof GetAssetByTimeBucketDto
      */
      */
     'userId'?: string;
     'userId'?: string;
+    /**
+     * Include assets without thumbnails
+     * @type {boolean}
+     * @memberof GetAssetByTimeBucketDto
+     */
+    'withoutThumbs'?: boolean;
 }
 }
 /**
 /**
  * 
  * 

+ 10 - 1
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -11,6 +11,7 @@
 	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
 	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
 	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
 	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
 	import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
 	import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
+	import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
 	import { fly } from 'svelte/transition';
 	import { fly } from 'svelte/transition';
 	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
 	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
 	import {
 	import {
@@ -350,7 +351,15 @@
 
 
 	<div class="row-start-1 row-span-full col-start-1 col-span-4">
 	<div class="row-start-1 row-span-full col-start-1 col-span-4">
 		{#key asset.id}
 		{#key asset.id}
-			{#if asset.type === AssetTypeEnum.Image}
+			{#if !asset.resizePath}
+				<div class="h-full w-full flex justify-center">
+					<div
+						class="h-full bg-gray-100 dark:bg-immich-dark-gray flex items-center justify-center aspect-square px-auto"
+					>
+						<ImageBrokenVariant size="25%" />
+					</div>
+				</div>
+			{:else if asset.type === AssetTypeEnum.Image}
 				{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
 				{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
 					<VideoViewer
 					<VideoViewer
 						{publicSharedKey}
 						{publicSharedKey}

+ 15 - 7
web/src/lib/components/assets/thumbnail/thumbnail.svelte

@@ -10,6 +10,7 @@
 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
 	import ImageThumbnail from './image-thumbnail.svelte';
 	import ImageThumbnail from './image-thumbnail.svelte';
 	import VideoThumbnail from './video-thumbnail.svelte';
 	import VideoThumbnail from './video-thumbnail.svelte';
+	import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
@@ -101,7 +102,7 @@
 			</div>
 			</div>
 
 
 			<div
 			<div
-				class="bg-gray-100 dark:bg-immich-dark-gray absolute select-none transition-transform"
+				class="h-full w-full bg-gray-100 dark:bg-immich-dark-gray absolute select-none transition-transform"
 				class:scale-[0.85]={selected}
 				class:scale-[0.85]={selected}
 			>
 			>
 				<!-- Gradient overlay on hover -->
 				<!-- Gradient overlay on hover -->
@@ -121,12 +122,19 @@
 						<ArchiveArrowDownOutline size="24" class="text-white" />
 						<ArchiveArrowDownOutline size="24" class="text-white" />
 					</div>
 					</div>
 				{/if}
 				{/if}
-				<ImageThumbnail
-					url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
-					altText={asset.originalFileName}
-					widthStyle="{width}px"
-					heightStyle="{height}px"
-				/>
+
+				{#if asset.resizePath}
+					<ImageThumbnail
+						url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
+						altText={asset.originalFileName}
+						widthStyle="{width}px"
+						heightStyle="{height}px"
+					/>
+				{:else}
+					<div class="w-full h-full p-4 flex items-center justify-center">
+						<ImageBrokenVariant size="48" />
+					</div>
+				{/if}
 
 
 				{#if asset.type === AssetTypeEnum.Video}
 				{#if asset.type === AssetTypeEnum.Video}
 					<div class="absolute w-full h-full top-0">
 					<div class="absolute w-full h-full top-0">

+ 2 - 1
web/src/lib/stores/assets.store.ts

@@ -67,7 +67,8 @@ function createAssetStore() {
 			const { data: assets } = await api.assetApi.getAssetByTimeBucket(
 			const { data: assets } = await api.assetApi.getAssetByTimeBucket(
 				{
 				{
 					timeBucket: [bucket],
 					timeBucket: [bucket],
-					userId: _assetGridState.userId
+					userId: _assetGridState.userId,
+					withoutThumbs: true
 				},
 				},
 				{ signal: currentBucketData?.cancelToken.signal }
 				{ signal: currentBucketData?.cancelToken.signal }
 			);
 			);