diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 63739dbd8b..ba920d6d13 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -5111,13 +5111,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} timeBucket * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getByTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -5155,6 +5156,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['albumId'] = albumId; } + if (personId !== undefined) { + localVarQueryParameter['personId'] = personId; + } + if (isArchived !== undefined) { localVarQueryParameter['isArchived'] = isArchived; } @@ -5430,13 +5435,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {TimeBucketSize} size * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -5472,6 +5478,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['albumId'] = albumId; } + if (personId !== undefined) { + localVarQueryParameter['personId'] = personId; + } + if (isArchived !== undefined) { localVarQueryParameter['isArchived'] = isArchived; } @@ -5986,14 +5996,15 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} timeBucket * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options); + async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6055,14 +6066,15 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {TimeBucketSize} size * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6256,7 +6268,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -6308,7 +6320,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * Get all asset of a device that are in the database, ID only. @@ -6625,6 +6637,13 @@ export interface AssetApiGetByTimeBucketRequest { */ readonly albumId?: string + /** + * + * @type {string} + * @memberof AssetApiGetByTimeBucket + */ + readonly personId?: string + /** * * @type {boolean} @@ -6758,6 +6777,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly albumId?: string + /** + * + * @type {string} + * @memberof AssetApiGetTimeBuckets + */ + readonly personId?: string + /** * * @type {boolean} @@ -7111,7 +7137,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -7175,7 +7201,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index b86097feab..2999f2de2a 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index a82c8ffc46..6f4c391bf8 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index c2e9a5a788..588902a14c 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 7c83acb84d..b94c554dde 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1684,6 +1684,15 @@ "type": "string" } }, + { + "name": "personId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "isArchived", "required": false, @@ -1787,6 +1796,15 @@ "type": "string" } }, + { + "name": "personId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "isArchived", "required": false, diff --git a/server/src/domain/asset/asset.repository.ts b/server/src/domain/asset/asset.repository.ts index 6bc5d1f038..1a5ded4263 100644 --- a/server/src/domain/asset/asset.repository.ts +++ b/server/src/domain/asset/asset.repository.ts @@ -57,6 +57,7 @@ export interface TimeBucketOptions { isArchived?: boolean; isFavorite?: boolean; albumId?: string; + personId?: string; } export interface TimeBucketItem { diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index c42366f39a..d3c991c15e 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -16,6 +16,9 @@ export class TimeBucketDto { @ValidateUUID({ optional: true }) albumId?: string; + @ValidateUUID({ optional: true }) + personId?: string; + @IsOptional() @IsBoolean() @Transform(toBoolean) diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index e5f997f56c..0fa8d8aa43 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -386,7 +386,7 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(userId: string, options: TimeBucketOptions) { - const { isArchived, isFavorite, albumId } = options; + const { isArchived, isFavorite, albumId, personId } = options; let builder = this.repository .createQueryBuilder('asset') @@ -406,6 +406,13 @@ export class AssetRepository implements IAssetRepository { builder = builder.andWhere('asset.isFavorite = :isFavorite', { isFavorite }); } + if (personId !== undefined) { + builder = builder + .innerJoin('asset.faces', 'faces') + .innerJoin('faces.person', 'person') + .andWhere('person.id = :personId', { personId }); + } + return builder; } } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 55549dba1b..7d83aa12da 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -5120,13 +5120,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} timeBucket * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getByTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -5164,6 +5165,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['albumId'] = albumId; } + if (personId !== undefined) { + localVarQueryParameter['personId'] = personId; + } + if (isArchived !== undefined) { localVarQueryParameter['isArchived'] = isArchived; } @@ -5439,13 +5444,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {TimeBucketSize} size * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -5481,6 +5487,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['albumId'] = albumId; } + if (personId !== undefined) { + localVarQueryParameter['personId'] = personId; + } + if (isArchived !== undefined) { localVarQueryParameter['isArchived'] = isArchived; } @@ -5995,14 +6005,15 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} timeBucket * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options); + async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6064,14 +6075,15 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {TimeBucketSize} size * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6276,14 +6288,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {string} timeBucket * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise> { - return localVarFp.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); + getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise> { + return localVarFp.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); }, /** * @@ -6339,14 +6352,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {TimeBucketSize} size * @param {string} [userId] * @param {string} [albumId] + * @param {string} [personId] * @param {boolean} [isArchived] * @param {boolean} [isFavorite] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise> { - return localVarFp.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); + getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise> { + return localVarFp.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); }, /** * Get all asset of a device that are in the database, ID only. @@ -6679,6 +6693,13 @@ export interface AssetApiGetByTimeBucketRequest { */ readonly albumId?: string + /** + * + * @type {string} + * @memberof AssetApiGetByTimeBucket + */ + readonly personId?: string + /** * * @type {boolean} @@ -6812,6 +6833,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly albumId?: string + /** + * + * @type {string} + * @memberof AssetApiGetTimeBuckets + */ + readonly personId?: string + /** * * @type {boolean} @@ -7165,7 +7193,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -7229,7 +7257,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/app.css b/web/src/app.css index 595086731f..6cf41d300f 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -15,6 +15,8 @@ :root { font-family: 'Work Sans', sans-serif; + /* Used by layouts to ensure proper spacing between navbar and content */ + --navbar-height: calc(theme(spacing.18) + 4px); } html { diff --git a/web/src/lib/components/album-page/asset-selection.svelte b/web/src/lib/components/album-page/asset-selection.svelte index 2035256942..bac3746649 100644 --- a/web/src/lib/components/album-page/asset-selection.svelte +++ b/web/src/lib/components/album-page/asset-selection.svelte @@ -69,6 +69,6 @@
- +
diff --git a/web/src/lib/components/faces-page/face-thumbnail-selector.svelte b/web/src/lib/components/faces-page/face-thumbnail-selector.svelte deleted file mode 100644 index f5d8d92730..0000000000 --- a/web/src/lib/components/faces-page/face-thumbnail-selector.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- - Select feature photo - -
- -
-
diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 6313e3340e..d9a6c7b2c0 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -118,7 +118,7 @@
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index 56beed9f5c..38cd799148 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -1,13 +1,13 @@ -
-
-
-

- Merge faces - {title} -

- dispatch('close')} /> -
+ dispatch('close')}> +
+
+
+

+ Merge faces - {title} +

+ dispatch('close')} /> +
-
- {#if !choosePersonToMerge} -
- -
-
- ([personMerge1, personMerge2] = [personMerge2, personMerge1])} - /> -
- - - {:else} -
-
- +
+ {#if !choosePersonToMerge} +
+
-
-
- {#each potentialMergePeople as person (person.id)} -
- -
- {/each} +
+ ([personMerge1, personMerge2] = [personMerge2, personMerge1])} + /> +
+ + + {:else} +
+
+ +
+
+
+ {#each potentialMergePeople as person (person.id)} +
+ +
+ {/each} +
-
- {/if} -
+ {/if} +
-
-

Are these the same face?

-
-
-

They will be merged together

-
-
- - +
+

Are these the same face?

+
+
+

They will be merged together

+
+
+ + +
-
+ diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 5aee804b8b..2cabebb658 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -18,8 +18,9 @@ export let assets: AssetResponseDto[]; export let bucketDate: string; export let bucketHeight: number; - export let isAlbumSelectionMode = false; + export let isSelectionMode = false; export let viewport: Viewport; + export let singleSelect = false; export let assetStore: AssetStore; export let assetInteractionStore: AssetInteractionStore; @@ -90,16 +91,12 @@ assetsInDateGroup: AssetResponseDto[], dateGroupTitle: string, ) => { - if (isAlbumSelectionMode) { + if (isSelectionMode || $isMultiSelectState) { assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); return; } - if ($isMultiSelectState) { - assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); - } else { - assetViewingStore.setAssetId(asset.id); - } + assetViewingStore.setAssetId(asset.id); }; const selectAssetGroupHandler = (selectAssetGroupHandler: AssetResponseDto[], dateGroupTitle: string) => { @@ -166,7 +163,7 @@ class="mb-2 flex h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm" style="width: {geometry[groupIndex].containerWidth}px" > - {#if (hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle)} + {#if !singleSelect && ((hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle))}
handleKeyboardPress(event); + const dispatch = createEventDispatcher<{ select: AssetResponseDto }>(); onMount(async () => { document.addEventListener('keydown', onKeyboardPress); @@ -173,11 +175,17 @@ }; const handleSelectAssets = async (e: CustomEvent) => { - const asset = e.detail.asset; + const asset = e.detail.asset as AssetResponseDto; if (!asset) { return; } + dispatch('select', asset); + + if (singleSelect) { + element.scrollTop = 0; + } + const rangeSelection = $assetSelectionCandidates.size > 0; const deselect = $selectedAssets.has(asset); @@ -308,7 +316,8 @@
- - diff --git a/web/src/routes/(user)/people/[personId]/+page.server.ts b/web/src/routes/(user)/people/[personId]/+page.server.ts index bfe80031e7..2b950d8588 100644 --- a/web/src/routes/(user)/people/[personId]/+page.server.ts +++ b/web/src/routes/(user)/people/[personId]/+page.server.ts @@ -9,12 +9,10 @@ export const load = (async ({ locals, parent, params }) => { } const { data: person } = await locals.api.personApi.getPerson({ id: params.personId }); - const { data: assets } = await locals.api.personApi.getPersonAssets({ id: params.personId }); const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: false }); return { user, - assets, person, people, meta: { diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index 160065513f..1ee8af573a 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -3,59 +3,68 @@ import { page } from '$app/stores'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; + import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; + import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; + import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; - import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; - import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import { AppRoute } from '$lib/constants'; - import { handleError } from '$lib/utils/handle-error'; - import { AssetResponseDto, PersonResponseDto, api } from '@api'; - import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; - import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; - import Plus from 'svelte-material-icons/Plus.svelte'; - import type { PageData } from './$types'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; - import FaceThumbnailSelector from '$lib/components/faces-page/face-thumbnail-selector.svelte'; + import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import { NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; - import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; + import { AppRoute } from '$lib/constants'; + import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; + import { AssetStore } from '$lib/stores/assets.store'; + import { handleError } from '$lib/utils/handle-error'; + import { AssetResponseDto, PersonResponseDto, TimeBucketSize, api } from '@api'; import { onMount } from 'svelte'; - import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; + import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; + import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; + import Plus from 'svelte-material-icons/Plus.svelte'; + import type { PageData } from './$types'; export let data: PageData; + + enum ViewMode { + VIEW_ASSETS = 'view-assets', + SELECT_FACE = 'select-face', + MERGE_FACES = 'merge-faces', + SUGGEST_MERGE = 'suggest-merge', + } + + const assetStore = new AssetStore({ + size: TimeBucketSize.Month, + isArchived: false, + personId: data.person.id, + }); + const assetInteractionStore = createAssetInteractionStore(); + const { selectedAssets, isMultiSelectState } = assetInteractionStore; + + let viewMode: ViewMode = ViewMode.VIEW_ASSETS; let isEditingName = false; - let showFaceThumbnailSelection = false; - let showMergeFacePanel = false; let previousRoute: string = AppRoute.EXPLORE; - let selectedAssets: Set = new Set(); - let showMergeModal = false; let people = data.people.people; let personMerge1: PersonResponseDto; let personMerge2: PersonResponseDto; let personName = ''; - $: isMultiSelectionMode = selectedAssets.size > 0; - $: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived); - $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); - - $: showAssets = !showMergeFacePanel && !showFaceThumbnailSelection; + $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived); + $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); onMount(() => { const action = $page.url.searchParams.get('action'); if (action == 'merge') { - showMergeFacePanel = true; + viewMode = ViewMode.MERGE_FACES; } }); afterNavigate(({ from }) => { @@ -65,35 +74,29 @@ } }); - const onAssetDelete = (assetId: string) => { - data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId); - }; - const handleSelectAll = () => { - selectedAssets = new Set(data.assets); - }; - - const handleSelectFeaturePhoto = async (event: CustomEvent) => { - showFaceThumbnailSelection = false; - - const { selectedAsset }: { selectedAsset: AssetResponseDto | undefined } = event.detail; - - if (selectedAsset) { - await api.personApi.updatePerson({ - id: data.person.id, - personUpdateDto: { featureFaceAssetId: selectedAsset.id }, - }); - - // TODO: Replace by Websocket in the future - notificationController.show({ - message: 'Feature photo updated, refresh page to see changes', - type: NotificationType.Info, - }); + const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => { + if (viewMode !== ViewMode.SELECT_FACE) { + return; } + + await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); + + // TODO: Replace by Websocket in the future + notificationController.show({ + message: 'Feature photo updated, refresh page to see changes', + type: NotificationType.Info, + }); + + assetInteractionStore.clearMultiselect(); + // scroll to top + + viewMode = ViewMode.VIEW_ASSETS; }; const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => { const [personToMerge, personToBeMergedIn] = response; - showMergeModal = false; + viewMode = ViewMode.VIEW_ASSETS; + isEditingName = false; try { await api.personApi.mergePerson({ id: personToBeMergedIn.id, @@ -116,7 +119,7 @@ }; const changeName = async () => { - showMergeModal = false; + viewMode = ViewMode.VIEW_ASSETS; data.person.name = personName; try { isEditingName = false; @@ -142,6 +145,14 @@ } }; + const handleCancelEditName = () => { + if (viewMode === ViewMode.SUGGEST_MERGE) { + return; + } + + isEditingName = false; + }; + const handleNameChange = async (name: string) => { personName = name; @@ -156,102 +167,111 @@ if (existingPerson) { personMerge2 = existingPerson; personMerge1 = data.person; - showMergeModal = true; + viewMode = ViewMode.SUGGEST_MERGE; return; } changeName(); }; -{#if showMergeModal} - (showMergeModal = false)}> - (showMergeModal = false)} - on:reject={() => changeName()} - on:confirm={(event) => handleMergeSameFace(event.detail)} - /> - +{#if viewMode === ViewMode.SUGGEST_MERGE} + (viewMode = ViewMode.VIEW_ASSETS)} + on:reject={() => changeName()} + on:confirm={(event) => handleMergeSameFace(event.detail)} + /> {/if} -{#if isMultiSelectionMode} - (selectedAssets = new Set())}> - - - - - - - - - - - onAssetDelete(asset.id)} /> - - -{:else} - goto(previousRoute)}> - - - (showFaceThumbnailSelection = true)} /> - (showMergeFacePanel = true)} /> +{#if viewMode === ViewMode.MERGE_FACES} + (viewMode = ViewMode.VIEW_ASSETS)} /> +{/if} + +
+ {#if $isMultiSelectState} + assetInteractionStore.clearMultiselect()}> + + + + + - - -{/if} - - -
- {#if isEditingName} - handleNameChange(event.detail)} - on:cancel={() => (isEditingName = false)} - /> + $assetStore.removeAsset(assetId)} /> + + + + $assetStore.removeAsset(asset.id)} + /> + + {:else} - + {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} + goto(previousRoute)}> + + + (viewMode = ViewMode.SELECT_FACE)} /> + (viewMode = ViewMode.MERGE_FACES)} /> + + + + {/if} - + {#if viewMode === ViewMode.SELECT_FACE} + (viewMode = ViewMode.VIEW_ASSETS)}> + Select feature photo + + {/if} {/if} -
+
- -{#if showAssets} -
-
-
- +
+ handleSelectFeaturePhoto(asset)} + > + {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} + +
+ {#if isEditingName} + handleNameChange(event.detail)} + on:cancel={() => handleCancelEditName()} + /> + {:else} + + + + {/if}
-
-
-{/if} - -{#if showFaceThumbnailSelection} - -{/if} - -{#if showMergeFacePanel} - (showMergeFacePanel = false)} /> -{/if} + {/if} + +