Compare commits
31 commits
feat/rotat
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
e1739ac4fc | ||
|
8736c77f7a | ||
|
338a028185 | ||
|
e2d0e944eb | ||
|
f53b70571b | ||
|
2814de4420 | ||
|
024fe1141b | ||
|
086a957a2b | ||
|
84c5b08c25 | ||
|
7e8488694d | ||
|
231b89c9c0 | ||
|
d5f6584e1d | ||
|
7702560b12 | ||
|
982183600d | ||
|
933c24ea6f | ||
|
05e9697dff | ||
|
259700c45f | ||
|
22d79850f6 | ||
|
56aed8246d | ||
|
ca1be71bca | ||
|
6111bf157e | ||
|
2195730fa6 | ||
|
1dc832d392 | ||
|
1a63d3837e | ||
|
bdbaa166d9 | ||
|
812e67d55d | ||
|
dfd6846deb | ||
|
6d3421a505 | ||
|
ede9de146a | ||
|
6959cf689b | ||
|
36ba48b8ae |
148 changed files with 6690 additions and 690 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -213,7 +213,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c
|
||||
image: postgres@sha256:6dfee32131933ab4ca25a00360c3f427fdc134de56f9a90c6c9a4956b48aea85
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
|
|
591
cli/src/api/open-api/api.ts
generated
591
cli/src/api/open-api/api.ts
generated
|
@ -586,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = {
|
|||
|
||||
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceResponseDto
|
||||
*/
|
||||
export interface AssetFaceResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
/**
|
||||
*
|
||||
* @type {PersonResponseDto}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'person': PersonResponseDto | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateDto
|
||||
*/
|
||||
export interface AssetFaceUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceUpdateItem>}
|
||||
* @memberof AssetFaceUpdateDto
|
||||
*/
|
||||
'data': Array<AssetFaceUpdateItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateItem
|
||||
*/
|
||||
export interface AssetFaceUpdateItem {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'assetId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'personId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
export interface AssetFaceWithoutPersonResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -842,10 +978,10 @@ export interface AssetResponseDto {
|
|||
'ownerId': string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<PersonResponseDto>}
|
||||
* @type {Array<PersonWithFacesResponseDto>}
|
||||
* @memberof AssetResponseDto
|
||||
*/
|
||||
'people'?: Array<PersonResponseDto>;
|
||||
'people'?: Array<PersonWithFacesResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
@ -1672,6 +1808,19 @@ export interface ExifResponseDto {
|
|||
*/
|
||||
'timeZone'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface FaceDto
|
||||
*/
|
||||
export interface FaceDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceDto
|
||||
*/
|
||||
'id': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -1785,7 +1934,8 @@ export const JobCommand = {
|
|||
Start: 'start',
|
||||
Pause: 'pause',
|
||||
Resume: 'resume',
|
||||
Empty: 'empty'
|
||||
Empty: 'empty',
|
||||
ClearFailed: 'clear-failed'
|
||||
} as const;
|
||||
|
||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
||||
|
@ -2563,6 +2713,49 @@ export interface PersonUpdateDto {
|
|||
*/
|
||||
'name'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PersonWithFacesResponseDto
|
||||
*/
|
||||
export interface PersonWithFacesResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'birthDate': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceWithoutPersonResponseDto>}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'faces': Array<AssetFaceWithoutPersonResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'isHidden': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'thumbnailPath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -11348,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* FaceApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getFaces', 'id', id)
|
||||
const localVarPath = `/face`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (id !== undefined) {
|
||||
localVarQueryParameter['id'] = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'id', id)
|
||||
// verify required parameter 'faceDto' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'faceDto', faceDto)
|
||||
const localVarPath = `/face/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetFaceResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = FaceApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetFaceResponseDto>> {
|
||||
return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for getFaces operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiGetFacesRequest
|
||||
*/
|
||||
export interface FaceApiGetFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiGetFaces
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFacesById operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiReassignFacesByIdRequest
|
||||
*/
|
||||
export interface FaceApiReassignFacesByIdRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {FaceDto}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly faceDto: FaceDto
|
||||
}
|
||||
|
||||
/**
|
||||
* FaceApi - object-oriented interface
|
||||
* @export
|
||||
* @class FaceApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class FaceApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* JobApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -13179,6 +13599,44 @@ export class PartnerApi extends BaseAPI {
|
|||
*/
|
||||
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/person`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13438,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'id', id)
|
||||
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
|
||||
const localVarPath = `/person/{id}/reassign`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13540,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
export const PersonApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13601,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13632,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = PersonApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson(options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.createPerson(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13686,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
|||
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||
return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13798,6 +14341,27 @@ export interface PersonApiMergePersonRequest {
|
|||
readonly mergePersonDto: MergePersonDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFaces operation in PersonApi.
|
||||
* @export
|
||||
* @interface PersonApiReassignFacesRequest
|
||||
*/
|
||||
export interface PersonApiReassignFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {AssetFaceUpdateDto}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly assetFaceUpdateDto: AssetFaceUpdateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePeople operation in PersonApi.
|
||||
* @export
|
||||
|
@ -13840,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest {
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class PersonApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public createPerson(options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13906,6 +14480,17 @@ export class PersonApi extends BaseAPI {
|
|||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -112,7 +112,7 @@ services:
|
|||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
|
@ -70,7 +70,7 @@ services:
|
|||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
|
@ -23,7 +23,7 @@ services:
|
|||
- database
|
||||
|
||||
database:
|
||||
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
command: -c fsync=off
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
|
|
@ -74,7 +74,7 @@ services:
|
|||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-bookworm@sha256:47c1829f72432c33609b3095259843a88c7ffc42cc9dbb55c43f2e7bbe46ca58 as builder
|
||||
FROM python:3.11-bookworm@sha256:ba7a7ac30c38e119c4304f98ef0e188f90f4f67a958bb6899da9defb99bfb471 as builder
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
|
@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
|||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:8f82989e563d0dbad057a874a96438a360978c148e34f36c1db8d2d61b5fd6f0
|
||||
FROM python:3.11-slim-bookworm@sha256:cc758519481092eb5a4a5ab0c1b303e288880d59afc601958d19e95b300bc86b
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:e296d47be09fc5d260eba9b191f60496f028a4f3ec41e8a14d48c0bae2c60244 as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
|
|
@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
|
||||
distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
|
|
@ -144,6 +144,8 @@
|
|||
"control_bottom_app_bar_stack": "Stack",
|
||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||
"control_bottom_app_bar_upload": "Upload",
|
||||
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||
"create_album_page_untitled": "Untitled",
|
||||
"create_shared_album_page_create": "Create",
|
||||
"create_shared_album_page_share": "Share",
|
||||
|
@ -165,6 +167,7 @@
|
|||
"exif_bottom_sheet_description": "Add Description...",
|
||||
"exif_bottom_sheet_details": "DETAILS",
|
||||
"exif_bottom_sheet_location": "LOCATION",
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||
"experimental_settings_subtitle": "Use at your own risk!",
|
||||
|
@ -461,5 +464,18 @@
|
|||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"scaffold_body_error_occured": "Error occured"
|
||||
"scaffold_body_error_occurred": "Error occurred",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"action_common_cancel": "Cancel",
|
||||
"action_common_update": "Update",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"map_location_picker_page_use_location": "Use this location",
|
||||
"location_picker_choose_on_map": "Choose on map",
|
||||
"location_picker_latitude": "Latitude",
|
||||
"location_picker_latitude_hint": "Enter your latitude here",
|
||||
"location_picker_latitude_error": "Enter a valid latitude",
|
||||
"location_picker_longitude": "Longitude",
|
||||
"location_picker_longitude_hint": "Enter your longitude here",
|
||||
"location_picker_longitude_error": "Enter a valid longitude"
|
||||
}
|
||||
|
|
36
mobile/lib/extensions/asset_extensions.dart
Normal file
36
mobile/lib/extensions/asset_extensions.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
extension TZExtension on Asset {
|
||||
/// Returns the created time of the asset from the exif info (if available) or from
|
||||
/// the fileCreatedAt field, adjusted to the timezone value from the exif info along with
|
||||
/// the timezone offset in [Duration]
|
||||
(DateTime, Duration) getTZAdjustedTimeAndOffset() {
|
||||
DateTime dt = fileCreatedAt.toLocal();
|
||||
if (exifInfo?.dateTimeOriginal != null) {
|
||||
dt = exifInfo!.dateTimeOriginal!;
|
||||
if (exifInfo?.timeZone != null) {
|
||||
dt = dt.toUtc();
|
||||
try {
|
||||
final location = getLocation(exifInfo!.timeZone!);
|
||||
dt = TZDateTime.from(dt, location);
|
||||
} on LocationNotFoundException {
|
||||
RegExp re = RegExp(
|
||||
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final m = re.firstMatch(exifInfo!.timeZone!);
|
||||
if (m != null) {
|
||||
final duration = Duration(
|
||||
hours: int.parse(m.group(1) ?? '0'),
|
||||
minutes: int.parse(m.group(2) ?? '0'),
|
||||
);
|
||||
dt = dt.add(duration);
|
||||
return (dt, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (dt, dt.timeZoneOffset);
|
||||
}
|
||||
}
|
4
mobile/lib/extensions/duration_extensions.dart
Normal file
4
mobile/lib/extensions/duration_extensions.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
extension TZOffsetExtension on Duration {
|
||||
String formatAsOffset() =>
|
||||
"${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
||||
}
|
|
@ -95,7 +95,11 @@ class ActivityStatisticsNotifier extends StateNotifier<int> {
|
|||
}
|
||||
|
||||
Future<void> fetchStatistics() async {
|
||||
state = await _activityService.getStatistics(albumId, assetId: assetId);
|
||||
final count =
|
||||
await _activityService.getStatistics(albumId, assetId: assetId);
|
||||
if (mounted) {
|
||||
state = count;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addActivity() async {
|
||||
|
|
|
@ -68,21 +68,20 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: album.thumbnail.value == null
|
||||
? buildEmptyThumbnail()
|
||||
: buildAlbumThumbnail(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
@ -110,6 +109,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -135,18 +135,24 @@ class LibraryPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
Widget buildCreateAlbumButton() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var cardSize = constraints.maxWidth;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
Container(
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkTheme
|
||||
|
@ -164,7 +170,6 @@ class LibraryPage extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
|
@ -179,6 +184,8 @@ class LibraryPage extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLibraryNavButton(
|
||||
|
|
|
@ -4,14 +4,15 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
|
||||
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
@ -21,53 +22,30 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
|
||||
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
|
||||
|
||||
bool hasCoordinates(ExifInfo? exifInfo) =>
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
||||
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
|
||||
bool hasCoordinates() =>
|
||||
exifInfo != null &&
|
||||
exifInfo.latitude != null &&
|
||||
exifInfo.longitude != null &&
|
||||
exifInfo.latitude != 0 &&
|
||||
exifInfo.longitude != 0;
|
||||
|
||||
String formatTimeZone(Duration d) =>
|
||||
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
String get formattedDateTime {
|
||||
DateTime dt = asset.fileCreatedAt.toLocal();
|
||||
String? timeZone;
|
||||
if (asset.exifInfo?.dateTimeOriginal != null) {
|
||||
dt = asset.exifInfo!.dateTimeOriginal!;
|
||||
if (asset.exifInfo?.timeZone != null) {
|
||||
dt = dt.toUtc();
|
||||
try {
|
||||
final location = getLocation(asset.exifInfo!.timeZone!);
|
||||
dt = TZDateTime.from(dt, location);
|
||||
} on LocationNotFoundException {
|
||||
RegExp re = RegExp(
|
||||
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final m = re.firstMatch(asset.exifInfo!.timeZone!);
|
||||
if (m != null) {
|
||||
final duration = Duration(
|
||||
hours: int.parse(m.group(1) ?? '0'),
|
||||
minutes: int.parse(m.group(2) ?? '0'),
|
||||
);
|
||||
dt = dt.add(duration);
|
||||
timeZone = formatTimeZone(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String formattedDateTime() {
|
||||
final (dt, timeZone) =
|
||||
(assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset();
|
||||
final date = DateFormat.yMMMEd().format(dt);
|
||||
final time = DateFormat.jm().format(dt);
|
||||
timeZone ??= formatTimeZone(dt.timeZoneOffset);
|
||||
|
||||
return '$date • $time $timeZone';
|
||||
return '$date • $time GMT${timeZone.formatAsOffset()}';
|
||||
}
|
||||
|
||||
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
|
||||
if (!hasCoordinates(exifInfo)) {
|
||||
Future<Uri?> createCoordinatesUri() async {
|
||||
if (!hasCoordinates()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -108,24 +86,20 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
||||
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
|
||||
buildMap() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return MapThumbnail(
|
||||
showAttribution: false,
|
||||
coords: LatLng(
|
||||
exifInfo?.latitude ?? 0,
|
||||
exifInfo?.longitude ?? 0,
|
||||
),
|
||||
height: 150,
|
||||
zoom: 16.0,
|
||||
width: constraints.maxWidth,
|
||||
zoom: 12.0,
|
||||
markers: [
|
||||
Marker(
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
|
@ -139,7 +113,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
),
|
||||
],
|
||||
onTap: (tapPosition, latLong) async {
|
||||
Uri? uri = await _createCoordinatesUri(exifInfo);
|
||||
Uri? uri = await createCoordinatesUri();
|
||||
|
||||
if (uri == null) {
|
||||
return;
|
||||
|
@ -181,8 +155,26 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
|
||||
buildLocation() {
|
||||
// Guard no lat/lng
|
||||
if (!hasCoordinates(exifInfo)) {
|
||||
return Container();
|
||||
if (!hasCoordinates()) {
|
||||
return asset.isRemote
|
||||
? ListTile(
|
||||
minLeadingWidth: 0,
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: Text(
|
||||
"exif_bottom_sheet_location_add",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
onTap: () => handleEditLocation(
|
||||
ref,
|
||||
context,
|
||||
[assetWithExif.value ?? asset],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
@ -190,14 +182,30 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
// Location
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"exif_bottom_sheet_location",
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
color:
|
||||
context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
).tr(),
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
onPressed: () => handleEditLocation(
|
||||
ref,
|
||||
context,
|
||||
[assetWithExif.value ?? asset],
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
buildMap(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
|
@ -233,12 +241,27 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
buildDate() {
|
||||
return Text(
|
||||
formattedDateTime,
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
formattedDateTime(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
onPressed: () => handleEditDateTime(
|
||||
ref,
|
||||
context,
|
||||
[assetWithExif.value ?? asset],
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -363,7 +386,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: hasCoordinates(exifInfo) ? 5 : 0,
|
||||
flex: hasCoordinates() ? 5 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: buildLocation(),
|
||||
|
@ -402,9 +425,8 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
buildLocation(),
|
||||
SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),
|
||||
SizedBox(height: hasCoordinates() ? 16.0 : 6.0),
|
||||
buildDetail(),
|
||||
const SizedBox(height: 50),
|
||||
],
|
||||
|
|
|
@ -795,6 +795,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
tag: isFromDto
|
||||
? '${a.remoteId}-$heroOffset'
|
||||
: a.id + heroOffset,
|
||||
transitionOnUserGestures: true,
|
||||
),
|
||||
filterQuality: FilterQuality.high,
|
||||
tightMode: true,
|
||||
|
|
|
@ -375,6 +375,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||
await _getBackupAlbumsInfo();
|
||||
await updateServerInfo();
|
||||
await _updateBackupAssetCount();
|
||||
} else {
|
||||
log.warning("cannot get backup info - background backup is in progress!");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||
final void Function() onCreateNewAlbum;
|
||||
final void Function() onUpload;
|
||||
final void Function() onStack;
|
||||
final void Function() onEditTime;
|
||||
final void Function() onEditLocation;
|
||||
|
||||
final List<Album> albums;
|
||||
final List<Album> sharedAlbums;
|
||||
|
@ -37,6 +39,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||
required this.onCreateNewAlbum,
|
||||
required this.onUpload,
|
||||
required this.onStack,
|
||||
required this.onEditTime,
|
||||
required this.onEditLocation,
|
||||
this.selectionAssetState = const SelectionAssetState(),
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
|
@ -74,6 +78,18 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||
label: "control_bottom_app_bar_favorite".tr(),
|
||||
onPressed: enabled ? onFavorite : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.edit_calendar_outlined,
|
||||
label: "control_bottom_app_bar_edit_time".tr(),
|
||||
onPressed: enabled ? onEditTime : null,
|
||||
),
|
||||
if (hasRemote)
|
||||
ControlBoxButton(
|
||||
iconData: Icons.edit_location_alt_outlined,
|
||||
label: "control_bottom_app_bar_edit_location".tr(),
|
||||
onPressed: enabled ? onEditLocation : null,
|
||||
),
|
||||
ControlBoxButton(
|
||||
iconData: Icons.delete_outline_rounded,
|
||||
label: "control_bottom_app_bar_delete".tr(),
|
||||
|
|
|
@ -312,6 +312,34 @@ class HomePage extends HookConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
void onEditTime() async {
|
||||
try {
|
||||
final remoteAssets = ownedRemoteSelection(
|
||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||
);
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditDateTime(ref, context, remoteAssets.toList());
|
||||
}
|
||||
} finally {
|
||||
selectionEnabledHook.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onEditLocation() async {
|
||||
try {
|
||||
final remoteAssets = ownedRemoteSelection(
|
||||
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||
);
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditLocation(ref, context, remoteAssets.toList());
|
||||
}
|
||||
} finally {
|
||||
selectionEnabledHook.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshAssets() async {
|
||||
final fullRefresh = refreshCount.value > 0;
|
||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||
|
@ -411,6 +439,8 @@ class HomePage extends HookConsumerWidget {
|
|||
enabled: !processing.value,
|
||||
selectionAssetState: selectionAssetState.value,
|
||||
onStack: onStack,
|
||||
onEditTime: onEditTime,
|
||||
onEditLocation: onEditLocation,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
|
@ -21,6 +23,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
AuthenticationNotifier(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._ref,
|
||||
) : super(
|
||||
AuthenticationState(
|
||||
deviceId: "",
|
||||
|
@ -36,6 +39,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final StateNotifierProviderRef<AuthenticationNotifier, AuthenticationState>
|
||||
_ref;
|
||||
final _log = Logger("AuthenticationNotifier");
|
||||
|
||||
Future<bool> login(
|
||||
|
@ -111,6 +116,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
Store.delete(StoreKey.currentUser),
|
||||
Store.delete(StoreKey.accessToken),
|
||||
]);
|
||||
_ref.invalidate(albumProvider);
|
||||
_ref.invalidate(sharedAlbumProvider);
|
||||
|
||||
state = state.copyWith(
|
||||
deviceId: "",
|
||||
|
@ -222,5 +229,6 @@ final authenticationProvider =
|
|||
return AuthenticationNotifier(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
|
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
|
@ -0,0 +1,113 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class MapLocationPickerPage extends HookConsumerWidget {
|
||||
final LatLng? initialLatLng;
|
||||
|
||||
const MapLocationPickerPage({super.key, this.initialLatLng});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedLatLng = useState<LatLng>(initialLatLng ?? LatLng(0, 0));
|
||||
final isDarkTheme =
|
||||
ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
|
||||
final isLoading =
|
||||
ref.watch(mapStateNotifier.select((state) => state.isLoading));
|
||||
final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
|
||||
|
||||
return Theme(
|
||||
// Override app theme based on map theme
|
||||
data: isDarkTheme ? immichDarkTheme : immichLightTheme,
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
if (!isLoading)
|
||||
FlutterMap(
|
||||
options: MapOptions(
|
||||
maxBounds:
|
||||
LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
|
||||
interactiveFlags: InteractiveFlag.doubleTapZoom |
|
||||
InteractiveFlag.drag |
|
||||
InteractiveFlag.flingAnimation |
|
||||
InteractiveFlag.pinchMove |
|
||||
InteractiveFlag.pinchZoom,
|
||||
center: LatLng(20, 20),
|
||||
zoom: 2,
|
||||
minZoom: 1,
|
||||
maxZoom: maxZoom,
|
||||
onTap: (tapPosition, point) => selectedLatLng.value = point,
|
||||
),
|
||||
children: [
|
||||
ref.read(mapStateNotifier.notifier).getTileLayer(),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
point: selectedLatLng.value,
|
||||
builder: (ctx) => const Image(
|
||||
image: AssetImage('assets/location-pin.png'),
|
||||
),
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isLoading)
|
||||
Positioned(
|
||||
top: context.height * 0.35,
|
||||
left: context.width * 0.425,
|
||||
child: const ImmichLoadingIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSheet: BottomSheet(
|
||||
onClosing: () {},
|
||||
builder: (context) => SizedBox(
|
||||
height: 150,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${selectedLatLng.value.latitude.toStringAsFixed(4)}, ${selectedLatLng.value.longitude.toStringAsFixed(4)}",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.autoPop(selectedLatLng.value),
|
||||
child: const Text("map_location_picker_page_use_location")
|
||||
.tr(),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.autoPop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.colorScheme.error,
|
||||
),
|
||||
child: const Text("action_common_cancel").tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/plugin_api.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
@ -12,13 +14,15 @@ class MapThumbnail extends HookConsumerWidget {
|
|||
final double zoom;
|
||||
final List<Marker> markers;
|
||||
final double height;
|
||||
final double width;
|
||||
final bool showAttribution;
|
||||
final bool isDarkTheme;
|
||||
|
||||
const MapThumbnail({
|
||||
super.key,
|
||||
required this.coords,
|
||||
required this.height,
|
||||
this.height = 100,
|
||||
this.width = 100,
|
||||
this.onTap,
|
||||
this.zoom = 1,
|
||||
this.showAttribution = true,
|
||||
|
@ -28,18 +32,33 @@ class MapThumbnail extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapController = useMapController();
|
||||
final isMapReady = useRef(false);
|
||||
ref.watch(mapStateNotifier.select((s) => s.mapStyle));
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (isMapReady.value && mapController.center != coords) {
|
||||
mapController.move(coords, zoom);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[coords],
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
interactiveFlags: InteractiveFlag.none,
|
||||
center: coords,
|
||||
zoom: zoom,
|
||||
onTap: onTap,
|
||||
onMapReady: () => isMapReady.value = true,
|
||||
),
|
||||
nonRotatedChildren: [
|
||||
if (showAttribution)
|
||||
|
|
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
MapController useMapController({
|
||||
String? debugLabel,
|
||||
List<Object?>? keys,
|
||||
}) {
|
||||
return use(_MapControllerHook(keys: keys));
|
||||
}
|
||||
|
||||
class _MapControllerHook extends Hook<MapController> {
|
||||
const _MapControllerHook({List<Object?>? keys}) : super(keys: keys);
|
||||
|
||||
@override
|
||||
HookState<MapController, Hook<MapController>> createState() =>
|
||||
_MapControllerHookState();
|
||||
}
|
||||
|
||||
class _MapControllerHookState
|
||||
extends HookState<MapController, _MapControllerHook> {
|
||||
late final controller = MapController();
|
||||
|
||||
@override
|
||||
MapController build(BuildContext context) => controller;
|
||||
|
||||
@override
|
||||
void dispose() => controller.dispose();
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useMapController';
|
||||
}
|
|
@ -55,6 +55,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
// in onMapEvent() since MapEventMove#id is not populated properly in the
|
||||
// current version of flutter_map(4.0.0) used
|
||||
bool forceAssetUpdate = false;
|
||||
bool isMapReady = false;
|
||||
late final Debounce debounce;
|
||||
|
||||
@override
|
||||
|
@ -79,7 +80,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
bool forceReload = false,
|
||||
}) {
|
||||
try {
|
||||
final bounds = mapController.bounds;
|
||||
final bounds = isMapReady ? mapController.bounds : null;
|
||||
if (bounds != null) {
|
||||
final oldAssetsInBounds = assetsInBounds.toSet();
|
||||
assetsInBounds =
|
||||
|
@ -455,6 +456,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
|||
minZoom: 1,
|
||||
maxZoom: maxZoom,
|
||||
onMapReady: () {
|
||||
isMapReady = true;
|
||||
mapController.mapEventStream.listen(onMapEvent);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'person.service.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b';
|
||||
String _$personServiceHash() => r'cde0a9c029d16ddde2adcd58ae8c863bf8cc1fed';
|
||||
|
||||
/// See also [personService].
|
||||
@ProviderFor(personService)
|
||||
|
|
|
@ -29,9 +29,8 @@ class CuratedPlacesRow extends CuratedRow {
|
|||
onTap: () => context.autoPush(
|
||||
const MapRoute(),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
child: SizedBox.square(
|
||||
dimension: imageSize,
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
|
@ -43,6 +42,7 @@ class CuratedPlacesRow extends CuratedRow {
|
|||
5,
|
||||
),
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
showAttribution: false,
|
||||
isDarkTheme: context.isDarkTheme,
|
||||
),
|
||||
|
|
|
@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$appSettingsServiceHash() =>
|
||||
r'957a65af6967701112f3076b507f9738fec4b7be';
|
||||
r'45ea609a91d250290431a7a08a14d16b37c7515d';
|
||||
|
||||
/// See also [appSettingsService].
|
||||
@ProviderFor(appSettingsService)
|
||||
|
|
31
mobile/lib/routing/custom_transition_builders.dart
Normal file
31
mobile/lib/routing/custom_transition_builders.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomTransitionsBuilders {
|
||||
const CustomTransitionsBuilders._();
|
||||
|
||||
static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder =
|
||||
ZoomPageTransitionsBuilder();
|
||||
|
||||
static const RouteTransitionsBuilder zoomedPage = _zoomedPage;
|
||||
|
||||
static Widget _zoomedPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return zoomPageTransitionsBuilder.buildTransitions(
|
||||
// Empty PageRoute<> object, only used to pass allowSnapshotting to ZoomPageTransitionsBuilder
|
||||
PageRouteBuilder(
|
||||
allowSnapshotting: true,
|
||||
fullscreenDialog: false,
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
context,
|
||||
animation,
|
||||
secondaryAnimation,
|
||||
child,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
|||
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
||||
import 'package:immich_mobile/modules/map/ui/map_location_picker.dart';
|
||||
import 'package:immich_mobile/modules/map/views/map_page.dart';
|
||||
import 'package:immich_mobile/modules/memories/models/memory.dart';
|
||||
import 'package:immich_mobile/modules/memories/views/memory_page.dart';
|
||||
|
@ -43,6 +44,7 @@ import 'package:immich_mobile/modules/search/views/search_page.dart';
|
|||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
@ -56,7 +58,8 @@ import 'package:immich_mobile/shared/views/app_log_page.dart';
|
|||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
||||
|
@ -86,9 +89,10 @@ part 'router.gr.dart';
|
|||
],
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
),
|
||||
AutoRoute(
|
||||
CustomRoute(
|
||||
page: GalleryViewerPage,
|
||||
guards: [AuthGuard, DuplicateGuard],
|
||||
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||
),
|
||||
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
|
||||
AutoRoute(
|
||||
|
@ -170,6 +174,10 @@ part 'router.gr.dart';
|
|||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
durationInMilliseconds: 200,
|
||||
),
|
||||
CustomRoute<LatLng?>(
|
||||
page: MapLocationPickerPage,
|
||||
guards: [AuthGuard, DuplicateGuard],
|
||||
),
|
||||
],
|
||||
)
|
||||
class AppRouter extends _$AppRouter {
|
||||
|
|
|
@ -63,7 +63,7 @@ class _$AppRouter extends RootStackRouter {
|
|||
},
|
||||
GalleryViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<GalleryViewerRouteArgs>();
|
||||
return MaterialPageX<dynamic>(
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: GalleryViewerPage(
|
||||
key: args.key,
|
||||
|
@ -75,6 +75,9 @@ class _$AppRouter extends RootStackRouter {
|
|||
isOwner: args.isOwner,
|
||||
sharedAlbumId: args.sharedAlbumId,
|
||||
),
|
||||
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
VideoViewerRoute.name: (routeData) {
|
||||
|
@ -357,6 +360,19 @@ class _$AppRouter extends RootStackRouter {
|
|||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
MapLocationPickerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<MapLocationPickerRouteArgs>(
|
||||
orElse: () => const MapLocationPickerRouteArgs());
|
||||
return CustomPage<LatLng?>(
|
||||
routeData: routeData,
|
||||
child: MapLocationPickerPage(
|
||||
key: args.key,
|
||||
initialLatLng: args.initialLatLng,
|
||||
),
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
HomeRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
|
@ -701,6 +717,14 @@ class _$AppRouter extends RootStackRouter {
|
|||
duplicateGuard,
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
MapLocationPickerRoute.name,
|
||||
path: '/map-location-picker-page',
|
||||
guards: [
|
||||
authGuard,
|
||||
duplicateGuard,
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1618,6 +1642,40 @@ class ActivitiesRouteArgs {
|
|||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [MapLocationPickerPage]
|
||||
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||
MapLocationPickerRoute({
|
||||
Key? key,
|
||||
LatLng? initialLatLng,
|
||||
}) : super(
|
||||
MapLocationPickerRoute.name,
|
||||
path: '/map-location-picker-page',
|
||||
args: MapLocationPickerRouteArgs(
|
||||
key: key,
|
||||
initialLatLng: initialLatLng,
|
||||
),
|
||||
);
|
||||
|
||||
static const String name = 'MapLocationPickerRoute';
|
||||
}
|
||||
|
||||
class MapLocationPickerRouteArgs {
|
||||
const MapLocationPickerRouteArgs({
|
||||
this.key,
|
||||
this.initialLatLng,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? initialLatLng;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [HomePage]
|
||||
class HomeRoute extends PageRouteInfo<void> {
|
||||
|
|
|
@ -256,6 +256,8 @@ class Asset {
|
|||
isFavorite != a.isFavorite ||
|
||||
isArchived != a.isArchived ||
|
||||
isTrashed != a.isTrashed ||
|
||||
a.exifInfo?.latitude != exifInfo?.latitude ||
|
||||
a.exifInfo?.longitude != exifInfo?.longitude ||
|
||||
// no local stack count or different count from remote
|
||||
((stackCount == null && a.stackCount != null) ||
|
||||
(stackCount != null &&
|
||||
|
|
2
mobile/lib/shared/providers/api.provider.g.dart
generated
2
mobile/lib/shared/providers/api.provider.g.dart
generated
|
@ -6,7 +6,7 @@ part of 'api.provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d';
|
||||
String _$apiServiceHash() => r'5b8beddb448316bdae5e3963ff77601653715729';
|
||||
|
||||
/// See also [apiService].
|
||||
@ProviderFor(apiService)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
|
@ -14,13 +17,33 @@ import 'package:socket_io_client/socket_io_client.dart';
|
|||
|
||||
enum PendingAction {
|
||||
assetDelete,
|
||||
assetUploaded,
|
||||
assetHidden,
|
||||
}
|
||||
|
||||
class PendingChange {
|
||||
final String id;
|
||||
final PendingAction action;
|
||||
final dynamic value;
|
||||
|
||||
const PendingChange(this.action, this.value);
|
||||
const PendingChange(
|
||||
this.id,
|
||||
this.action,
|
||||
this.value,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => 'PendingChange(id: $id, action: $action, value: $value)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PendingChange && other.id == id && other.action == action;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ action.hashCode;
|
||||
}
|
||||
|
||||
class WebsocketState {
|
||||
|
@ -131,6 +154,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||
socket.on('on_new_release', _handleReleaseUpdates);
|
||||
} catch (e) {
|
||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||
|
@ -163,34 +187,77 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
}
|
||||
|
||||
void addPendingChange(PendingAction action, dynamic value) {
|
||||
final now = DateTime.now();
|
||||
state = state.copyWith(
|
||||
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
|
||||
pendingChanges: [
|
||||
...state.pendingChanges,
|
||||
PendingChange(now.millisecondsSinceEpoch.toString(), action, value),
|
||||
],
|
||||
);
|
||||
_debounce(handlePendingChanges);
|
||||
}
|
||||
|
||||
void handlePendingChanges() {
|
||||
Future<void> _handlePendingDeletes() async {
|
||||
final deleteChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
.toList();
|
||||
if (deleteChanges.isNotEmpty) {
|
||||
List<String> remoteIds =
|
||||
deleteChanges.map((a) => a.value.toString()).toList();
|
||||
_ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.where((c) => c.action != PendingAction.assetDelete)
|
||||
.whereNot((c) => deleteChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleOnUploadSuccess(dynamic data) {
|
||||
final dto = AssetResponseDto.fromJson(data);
|
||||
Future<void> _handlePendingUploaded() async {
|
||||
final uploadedChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetUploaded)
|
||||
.toList();
|
||||
if (uploadedChanges.isNotEmpty) {
|
||||
List<AssetResponseDto?> remoteAssets = uploadedChanges
|
||||
.map((a) => AssetResponseDto.fromJson(a.value))
|
||||
.toList();
|
||||
for (final dto in remoteAssets) {
|
||||
if (dto != null) {
|
||||
final newAsset = Asset.remote(dto);
|
||||
_ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
}
|
||||
}
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.whereNot((c) => uploadedChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlingPendingHidden() async {
|
||||
final hiddenChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetHidden)
|
||||
.toList();
|
||||
if (hiddenChanges.isNotEmpty) {
|
||||
List<String> remoteIds =
|
||||
hiddenChanges.map((a) => a.value.toString()).toList();
|
||||
final db = _ref.watch(dbProvider);
|
||||
await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds));
|
||||
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.whereNot((c) => hiddenChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void handlePendingChanges() async {
|
||||
await _handlePendingUploaded();
|
||||
await _handlePendingDeletes();
|
||||
await _handlingPendingHidden();
|
||||
}
|
||||
|
||||
void _handleOnConfigUpdate(dynamic _) {
|
||||
_ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||
|
@ -202,10 +269,14 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
_ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
void _handleOnAssetDelete(dynamic data) {
|
||||
void _handleOnUploadSuccess(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetUploaded, data);
|
||||
|
||||
void _handleOnAssetDelete(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetDelete, data);
|
||||
_debounce(handlePendingChanges);
|
||||
}
|
||||
|
||||
void _handleOnAssetHidden(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetHidden, data);
|
||||
|
||||
_handleReleaseUpdates(dynamic data) {
|
||||
// Json guard
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
|
@ -181,4 +182,27 @@ class AssetService {
|
|||
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
|
||||
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeDateTime(
|
||||
List<Asset> assets,
|
||||
String updatedDt,
|
||||
) {
|
||||
return updateAssets(
|
||||
assets,
|
||||
UpdateAssetDto(dateTimeOriginal: updatedDt),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeLocation(
|
||||
List<Asset> assets,
|
||||
LatLng location,
|
||||
) {
|
||||
return updateAssets(
|
||||
assets,
|
||||
UpdateAssetDto(
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -401,6 +401,10 @@ class SyncService {
|
|||
|
||||
final Album a = await Album.remote(dto);
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
||||
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
260
mobile/lib/shared/ui/date_time_picker.dart
Normal file
260
mobile/lib/shared/ui/date_time_picker.dart
Normal file
|
@ -0,0 +1,260 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
Future<String?> showDateTimePicker({
|
||||
required BuildContext context,
|
||||
DateTime? initialDateTime,
|
||||
String? initialTZ,
|
||||
Duration? initialTZOffset,
|
||||
}) {
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
builder: (context) => _DateTimePicker(
|
||||
initialDateTime: initialDateTime,
|
||||
initialTZ: initialTZ,
|
||||
initialTZOffset: initialTZOffset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
||||
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||
}
|
||||
|
||||
class _DateTimePicker extends HookWidget {
|
||||
final DateTime? initialDateTime;
|
||||
final String? initialTZ;
|
||||
final Duration? initialTZOffset;
|
||||
|
||||
const _DateTimePicker({
|
||||
this.initialDateTime,
|
||||
this.initialTZ,
|
||||
this.initialTZOffset,
|
||||
});
|
||||
|
||||
_TimeZoneOffset _getInitiationLocation() {
|
||||
if (initialTZ != null) {
|
||||
try {
|
||||
return _TimeZoneOffset.fromLocation(
|
||||
tz.timeZoneDatabase.get(initialTZ!),
|
||||
);
|
||||
} on LocationNotFoundException {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset;
|
||||
|
||||
if (tzOffset != null) {
|
||||
final offsetInMilli = tzOffset.inMilliseconds;
|
||||
// get all locations with matching offset
|
||||
final locations = tz.timeZoneDatabase.locations.values.where(
|
||||
(location) => location.currentTimeZone.offset == offsetInMilli,
|
||||
);
|
||||
// Prefer locations with abbreviation first
|
||||
final location = locations.firstWhereOrNull(
|
||||
(e) => !e.currentTimeZone.abbreviation.contains("0"),
|
||||
) ??
|
||||
locations.firstOrNull;
|
||||
if (location != null) {
|
||||
return _TimeZoneOffset.fromLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
return _TimeZoneOffset.fromLocation(tz.getLocation("UTC"));
|
||||
}
|
||||
|
||||
// returns a list of location<name> along with it's offset in duration
|
||||
List<_TimeZoneOffset> getAllTimeZones() {
|
||||
return tz.timeZoneDatabase.locations.values
|
||||
.where((l) => !l.currentTimeZone.abbreviation.contains("0"))
|
||||
.map(_TimeZoneOffset.fromLocation)
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final date = useState<DateTime>(initialDateTime ?? DateTime.now());
|
||||
final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation());
|
||||
final timeZones = useMemoized(() => getAllTimeZones(), const []);
|
||||
|
||||
void pickDate() async {
|
||||
final now = DateTime.now();
|
||||
// Handles cases where the date from the asset is far off in the future
|
||||
final initialDate = date.value.isAfter(now) ? now : date.value;
|
||||
final newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initialDate,
|
||||
firstDate: DateTime(1800),
|
||||
lastDate: now,
|
||||
);
|
||||
if (newDate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(date.value),
|
||||
);
|
||||
|
||||
if (newTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
date.value = newDate.copyWith(hour: newTime.hour, minute: newTime.minute);
|
||||
}
|
||||
|
||||
void popWithDateTime() {
|
||||
final formattedDateTime =
|
||||
DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value);
|
||||
final dtWithOffset = formattedDateTime +
|
||||
Duration(milliseconds: tzOffset.value.offsetInMilliseconds)
|
||||
.formatAsOffset();
|
||||
context.pop(dtWithOffset);
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(30),
|
||||
alignment: Alignment.center,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"edit_date_time_dialog_date_time",
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
TextButton.icon(
|
||||
onPressed: pickDate,
|
||||
icon: Text(
|
||||
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
|
||||
style: context.textTheme.bodyLarge
|
||||
?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
label: const Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
"edit_date_time_dialog_timezone",
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
DropdownMenu(
|
||||
menuHeight: 300,
|
||||
width: 280,
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
trailingIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
textStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
|
||||
alignment: Alignment(-1.25, 0.5),
|
||||
),
|
||||
onSelected: (value) => tzOffset.value = value!,
|
||||
initialSelection: tzOffset.value,
|
||||
dropdownMenuEntries: timeZones
|
||||
.map(
|
||||
(t) => DropdownMenuEntry<_TimeZoneOffset>(
|
||||
value: t,
|
||||
label: t.display,
|
||||
style: ButtonStyle(
|
||||
textStyle: MaterialStatePropertyAll(
|
||||
context.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"action_common_cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: popWithDateTime,
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
|
||||
final String display;
|
||||
final Location location;
|
||||
|
||||
const _TimeZoneOffset({
|
||||
required this.display,
|
||||
required this.location,
|
||||
});
|
||||
|
||||
_TimeZoneOffset copyWith({
|
||||
String? display,
|
||||
Location? location,
|
||||
}) {
|
||||
return _TimeZoneOffset(
|
||||
display: display ?? this.display,
|
||||
location: location ?? this.location,
|
||||
);
|
||||
}
|
||||
|
||||
int get offsetInMilliseconds => location.currentTimeZone.offset;
|
||||
|
||||
_TimeZoneOffset.fromLocation(tz.Location l)
|
||||
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
|
||||
location = l;
|
||||
|
||||
@override
|
||||
int compareTo(_TimeZoneOffset other) {
|
||||
return offsetInMilliseconds.compareTo(other.offsetInMilliseconds);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'_TimeZoneOffset(display: $display, location: $location)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is _TimeZoneOffset &&
|
||||
other.display == display &&
|
||||
other.offsetInMilliseconds == offsetInMilliseconds;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode;
|
||||
}
|
256
mobile/lib/shared/ui/location_picker.dart
Normal file
256
mobile/lib/shared/ui/location_picker.dart
Normal file
|
@ -0,0 +1,256 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_map/plugin_api.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
Future<LatLng?> showLocationPicker({
|
||||
required BuildContext context,
|
||||
LatLng? initialLatLng,
|
||||
}) {
|
||||
return showDialog<LatLng?>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (ctx) => _LocationPicker(
|
||||
initialLatLng: initialLatLng,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
enum _LocationPickerMode { map, manual }
|
||||
|
||||
bool _validateLat(String value) {
|
||||
final l = double.tryParse(value);
|
||||
return l != null && l > -90 && l < 90;
|
||||
}
|
||||
|
||||
bool _validateLong(String value) {
|
||||
final l = double.tryParse(value);
|
||||
return l != null && l > -180 && l < 180;
|
||||
}
|
||||
|
||||
class _LocationPicker extends HookWidget {
|
||||
final LatLng? initialLatLng;
|
||||
|
||||
const _LocationPicker({
|
||||
this.initialLatLng,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final latitude = useState(initialLatLng?.latitude ?? 0.0);
|
||||
final longitude = useState(initialLatLng?.longitude ?? 0.0);
|
||||
final latlng = LatLng(latitude.value, longitude.value);
|
||||
final pickerMode = useState(_LocationPickerMode.map);
|
||||
final latitudeController = useTextEditingController();
|
||||
final isValidLatitude = useState(true);
|
||||
final latitiudeFocusNode = useFocusNode();
|
||||
final longitudeController = useTextEditingController();
|
||||
final longitudeFocusNode = useFocusNode();
|
||||
final isValidLongitude = useState(true);
|
||||
|
||||
void validateInputs() {
|
||||
isValidLatitude.value = _validateLat(latitudeController.text);
|
||||
if (isValidLatitude.value) {
|
||||
latitude.value = latitudeController.text.toDouble();
|
||||
}
|
||||
isValidLongitude.value = _validateLong(longitudeController.text);
|
||||
if (isValidLongitude.value) {
|
||||
longitude.value = longitudeController.text.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
void validateAndPop() {
|
||||
if (pickerMode.value == _LocationPickerMode.manual) {
|
||||
validateInputs();
|
||||
}
|
||||
if (isValidLatitude.value && isValidLongitude.value) {
|
||||
return context.pop(latlng);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> buildMapPickerMode() {
|
||||
return [
|
||||
TextButton.icon(
|
||||
icon: Text(
|
||||
"${latitude.value.toStringAsFixed(4)}, ${longitude.value.toStringAsFixed(4)}",
|
||||
),
|
||||
label: const Icon(Icons.edit_outlined, size: 16),
|
||||
onPressed: () {
|
||||
latitudeController.text = latitude.value.toStringAsFixed(4);
|
||||
longitudeController.text = longitude.value.toStringAsFixed(4);
|
||||
pickerMode.value = _LocationPickerMode.manual;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
MapThumbnail(
|
||||
coords: latlng,
|
||||
height: 200,
|
||||
width: 200,
|
||||
zoom: 6,
|
||||
showAttribution: false,
|
||||
onTap: (p0, p1) async {
|
||||
final newLatLng = await context.autoPush<LatLng?>(
|
||||
MapLocationPickerRoute(initialLatLng: latlng),
|
||||
);
|
||||
if (newLatLng != null) {
|
||||
latitude.value = newLatLng.latitude;
|
||||
longitude.value = newLatLng.longitude;
|
||||
}
|
||||
},
|
||||
markers: [
|
||||
Marker(
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
point: LatLng(
|
||||
latitude.value,
|
||||
longitude.value,
|
||||
),
|
||||
builder: (ctx) => const Image(
|
||||
image: AssetImage('assets/location-pin.png'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildManualPickerMode() {
|
||||
return [
|
||||
TextButton.icon(
|
||||
icon: const Text("location_picker_choose_on_map").tr(),
|
||||
label: const Icon(Icons.map_outlined, size: 16),
|
||||
onPressed: () {
|
||||
validateInputs();
|
||||
if (isValidLatitude.value && isValidLongitude.value) {
|
||||
pickerMode.value = _LocationPickerMode.map;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextField(
|
||||
controller: latitudeController,
|
||||
focusNode: latitiudeFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location_picker_latitude'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'location_picker_latitude_hint'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
errorText: isValidLatitude.value
|
||||
? null
|
||||
: "location_picker_latitude_error".tr(),
|
||||
),
|
||||
onEditingComplete: () {
|
||||
isValidLatitude.value = _validateLat(latitudeController.text);
|
||||
if (isValidLatitude.value) {
|
||||
latitude.value = latitudeController.text.toDouble();
|
||||
longitudeFocusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(8)],
|
||||
onTapOutside: (_) => latitiudeFocusNode.unfocus(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
TextField(
|
||||
controller: longitudeController,
|
||||
focusNode: longitudeFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location_picker_longitude'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'location_picker_longitude_hint'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
errorText: isValidLongitude.value
|
||||
? null
|
||||
: "location_picker_longitude_error".tr(),
|
||||
),
|
||||
onEditingComplete: () {
|
||||
isValidLongitude.value = _validateLong(longitudeController.text);
|
||||
if (isValidLongitude.value) {
|
||||
longitude.value = longitudeController.text.toDouble();
|
||||
longitudeFocusNode.unfocus();
|
||||
}
|
||||
},
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(8)],
|
||||
onTapOutside: (_) => longitudeFocusNode.unfocus(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(30),
|
||||
alignment: Alignment.center,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"edit_location_dialog_title",
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (pickerMode.value == _LocationPickerMode.manual)
|
||||
...buildManualPickerMode(),
|
||||
if (pickerMode.value == _LocationPickerMode.map)
|
||||
...buildMapPickerMode(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"action_common_cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: validateAndPop,
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ class ScaffoldErrorBody extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"scaffold_body_error_occured",
|
||||
"scaffold_body_error_occurred",
|
||||
style: context.textTheme.displayMedium,
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
|
|
|
@ -20,7 +20,7 @@ final immichThemeProvider = StateProvider<ThemeMode>((ref) {
|
|||
}
|
||||
});
|
||||
|
||||
ThemeData base = ThemeData(
|
||||
final ThemeData base = ThemeData(
|
||||
chipTheme: const ChipThemeData(
|
||||
side: BorderSide.none,
|
||||
),
|
||||
|
@ -30,7 +30,7 @@ ThemeData base = ThemeData(
|
|||
),
|
||||
);
|
||||
|
||||
ThemeData immichLightTheme = ThemeData(
|
||||
final ThemeData immichLightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.indigo,
|
||||
|
@ -153,7 +153,7 @@ ThemeData immichLightTheme = ThemeData(
|
|||
),
|
||||
);
|
||||
|
||||
ThemeData immichDarkTheme = ThemeData(
|
||||
final ThemeData immichDarkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.indigo,
|
||||
|
|
|
@ -2,12 +2,17 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/date_time_picker.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/ui/location_picker.dart';
|
||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
void handleShareAssets(
|
||||
WidgetRef ref,
|
||||
|
@ -85,3 +90,60 @@ Future<void> handleFavoriteAssets(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleEditDateTime(
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
List<Asset> selection,
|
||||
) async {
|
||||
DateTime? initialDate;
|
||||
String? timeZone;
|
||||
Duration? offset;
|
||||
if (selection.length == 1) {
|
||||
final asset = selection.first;
|
||||
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset();
|
||||
initialDate = dt;
|
||||
offset = oft;
|
||||
timeZone = assetWithExif.exifInfo?.timeZone;
|
||||
}
|
||||
final dateTime = await showDateTimePicker(
|
||||
context: context,
|
||||
initialDateTime: initialDate,
|
||||
initialTZ: timeZone,
|
||||
initialTZOffset: offset,
|
||||
);
|
||||
if (dateTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime);
|
||||
}
|
||||
|
||||
Future<void> handleEditLocation(
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
List<Asset> selection,
|
||||
) async {
|
||||
LatLng? initialLatLng;
|
||||
if (selection.length == 1) {
|
||||
final asset = selection.first;
|
||||
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
if (assetWithExif.exifInfo?.latitude != null &&
|
||||
assetWithExif.exifInfo?.longitude != null) {
|
||||
initialLatLng = LatLng(
|
||||
assetWithExif.exifInfo!.latitude!,
|
||||
assetWithExif.exifInfo!.longitude!,
|
||||
);
|
||||
}
|
||||
}
|
||||
final location = await showLocationPicker(
|
||||
context: context,
|
||||
initialLatLng: initialLatLng,
|
||||
);
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
|
||||
}
|
||||
|
|
21
mobile/openapi/.openapi-generator/FILES
generated
21
mobile/openapi/.openapi-generator/FILES
generated
|
@ -24,6 +24,10 @@ doc/AssetBulkUploadCheckDto.md
|
|||
doc/AssetBulkUploadCheckItem.md
|
||||
doc/AssetBulkUploadCheckResponseDto.md
|
||||
doc/AssetBulkUploadCheckResult.md
|
||||
doc/AssetFaceResponseDto.md
|
||||
doc/AssetFaceUpdateDto.md
|
||||
doc/AssetFaceUpdateItem.md
|
||||
doc/AssetFaceWithoutPersonResponseDto.md
|
||||
doc/AssetFileUploadResponseDto.md
|
||||
doc/AssetIdsDto.md
|
||||
doc/AssetIdsResponseDto.md
|
||||
|
@ -60,6 +64,8 @@ doc/DownloadInfoDto.md
|
|||
doc/DownloadResponseDto.md
|
||||
doc/EntityType.md
|
||||
doc/ExifResponseDto.md
|
||||
doc/FaceApi.md
|
||||
doc/FaceDto.md
|
||||
doc/FileChecksumDto.md
|
||||
doc/FileChecksumResponseDto.md
|
||||
doc/FileReportDto.md
|
||||
|
@ -100,6 +106,7 @@ doc/PersonApi.md
|
|||
doc/PersonResponseDto.md
|
||||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/PersonWithFacesResponseDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionLevel.md
|
||||
doc/ReactionType.md
|
||||
|
@ -177,6 +184,7 @@ lib/api/api_key_api.dart
|
|||
lib/api/asset_api.dart
|
||||
lib/api/audit_api.dart
|
||||
lib/api/authentication_api.dart
|
||||
lib/api/face_api.dart
|
||||
lib/api/job_api.dart
|
||||
lib/api/library_api.dart
|
||||
lib/api/o_auth_api.dart
|
||||
|
@ -213,6 +221,10 @@ lib/model/asset_bulk_upload_check_dto.dart
|
|||
lib/model/asset_bulk_upload_check_item.dart
|
||||
lib/model/asset_bulk_upload_check_response_dto.dart
|
||||
lib/model/asset_bulk_upload_check_result.dart
|
||||
lib/model/asset_face_response_dto.dart
|
||||
lib/model/asset_face_update_dto.dart
|
||||
lib/model/asset_face_update_item.dart
|
||||
lib/model/asset_face_without_person_response_dto.dart
|
||||
lib/model/asset_file_upload_response_dto.dart
|
||||
lib/model/asset_ids_dto.dart
|
||||
lib/model/asset_ids_response_dto.dart
|
||||
|
@ -247,6 +259,7 @@ lib/model/download_info_dto.dart
|
|||
lib/model/download_response_dto.dart
|
||||
lib/model/entity_type.dart
|
||||
lib/model/exif_response_dto.dart
|
||||
lib/model/face_dto.dart
|
||||
lib/model/file_checksum_dto.dart
|
||||
lib/model/file_checksum_response_dto.dart
|
||||
lib/model/file_report_dto.dart
|
||||
|
@ -282,6 +295,7 @@ lib/model/people_update_item.dart
|
|||
lib/model/person_response_dto.dart
|
||||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/person_with_faces_response_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_level.dart
|
||||
lib/model/reaction_type.dart
|
||||
|
@ -367,6 +381,10 @@ test/asset_bulk_upload_check_dto_test.dart
|
|||
test/asset_bulk_upload_check_item_test.dart
|
||||
test/asset_bulk_upload_check_response_dto_test.dart
|
||||
test/asset_bulk_upload_check_result_test.dart
|
||||
test/asset_face_response_dto_test.dart
|
||||
test/asset_face_update_dto_test.dart
|
||||
test/asset_face_update_item_test.dart
|
||||
test/asset_face_without_person_response_dto_test.dart
|
||||
test/asset_file_upload_response_dto_test.dart
|
||||
test/asset_ids_dto_test.dart
|
||||
test/asset_ids_response_dto_test.dart
|
||||
|
@ -403,6 +421,8 @@ test/download_info_dto_test.dart
|
|||
test/download_response_dto_test.dart
|
||||
test/entity_type_test.dart
|
||||
test/exif_response_dto_test.dart
|
||||
test/face_api_test.dart
|
||||
test/face_dto_test.dart
|
||||
test/file_checksum_dto_test.dart
|
||||
test/file_checksum_response_dto_test.dart
|
||||
test/file_report_dto_test.dart
|
||||
|
@ -443,6 +463,7 @@ test/person_api_test.dart
|
|||
test/person_response_dto_test.dart
|
||||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/person_with_faces_response_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_level_test.dart
|
||||
test/reaction_type_test.dart
|
||||
|
|
10
mobile/openapi/README.md
generated
10
mobile/openapi/README.md
generated
|
@ -133,6 +133,8 @@ Class | Method | HTTP request | Description
|
|||
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face |
|
||||
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
|
||||
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
||||
|
@ -153,12 +155,14 @@ Class | Method | HTTP request | Description
|
|||
*PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner |
|
||||
*PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
|
||||
*PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /partner/{id} |
|
||||
*PersonApi* | [**createPerson**](doc//PersonApi.md#createperson) | **POST** /person |
|
||||
*PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person |
|
||||
*PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} |
|
||||
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
*PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
|
||||
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||
|
@ -224,6 +228,10 @@ Class | Method | HTTP request | Description
|
|||
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
|
||||
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
|
||||
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
|
||||
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
|
||||
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
|
||||
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
|
||||
- [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md)
|
||||
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
|
||||
- [AssetIdsDto](doc//AssetIdsDto.md)
|
||||
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
|
||||
|
@ -258,6 +266,7 @@ Class | Method | HTTP request | Description
|
|||
- [DownloadResponseDto](doc//DownloadResponseDto.md)
|
||||
- [EntityType](doc//EntityType.md)
|
||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||
- [FaceDto](doc//FaceDto.md)
|
||||
- [FileChecksumDto](doc//FileChecksumDto.md)
|
||||
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
|
||||
- [FileReportDto](doc//FileReportDto.md)
|
||||
|
@ -293,6 +302,7 @@ Class | Method | HTTP request | Description
|
|||
- [PersonResponseDto](doc//PersonResponseDto.md)
|
||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
|
|
22
mobile/openapi/doc/AssetFaceResponseDto.md
generated
Normal file
22
mobile/openapi/doc/AssetFaceResponseDto.md
generated
Normal file
|
@ -0,0 +1,22 @@
|
|||
# openapi.model.AssetFaceResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**boundingBoxX1** | **int** | |
|
||||
**boundingBoxX2** | **int** | |
|
||||
**boundingBoxY1** | **int** | |
|
||||
**boundingBoxY2** | **int** | |
|
||||
**id** | **String** | |
|
||||
**imageHeight** | **int** | |
|
||||
**imageWidth** | **int** | |
|
||||
**person** | [**PersonResponseDto**](PersonResponseDto.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)
|
||||
|
||||
|
15
mobile/openapi/doc/AssetFaceUpdateDto.md
generated
Normal file
15
mobile/openapi/doc/AssetFaceUpdateDto.md
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
# openapi.model.AssetFaceUpdateDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**data** | [**List<AssetFaceUpdateItem>**](AssetFaceUpdateItem.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
16
mobile/openapi/doc/AssetFaceUpdateItem.md
generated
Normal file
16
mobile/openapi/doc/AssetFaceUpdateItem.md
generated
Normal file
|
@ -0,0 +1,16 @@
|
|||
# openapi.model.AssetFaceUpdateItem
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetId** | **String** | |
|
||||
**personId** | **String** | |
|
||||
|
||||
[[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
mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md
generated
Normal file
21
mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
# openapi.model.AssetFaceWithoutPersonResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**boundingBoxX1** | **int** | |
|
||||
**boundingBoxX2** | **int** | |
|
||||
**boundingBoxY1** | **int** | |
|
||||
**boundingBoxY2** | **int** | |
|
||||
**id** | **String** | |
|
||||
**imageHeight** | **int** | |
|
||||
**imageWidth** | **int** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
2
mobile/openapi/doc/AssetResponseDto.md
generated
2
mobile/openapi/doc/AssetResponseDto.md
generated
|
@ -30,7 +30,7 @@ Name | Type | Description | Notes
|
|||
**originalPath** | **String** | |
|
||||
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
|
||||
**ownerId** | **String** | |
|
||||
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
|
||||
**people** | [**List<PersonWithFacesResponseDto>**](PersonWithFacesResponseDto.md) | | [optional] [default to const []]
|
||||
**resized** | **bool** | |
|
||||
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
|
||||
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]
|
||||
|
|
127
mobile/openapi/doc/FaceApi.md
generated
Normal file
127
mobile/openapi/doc/FaceApi.md
generated
Normal file
|
@ -0,0 +1,127 @@
|
|||
# openapi.api.FaceApi
|
||||
|
||||
## Load the API package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**getFaces**](FaceApi.md#getfaces) | **GET** /face |
|
||||
[**reassignFacesById**](FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
|
||||
|
||||
|
||||
# **getFaces**
|
||||
> List<AssetFaceResponseDto> getFaces(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FaceApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getFaces(id);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FaceApi->getFaces: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<AssetFaceResponseDto>**](AssetFaceResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **reassignFacesById**
|
||||
> PersonResponseDto reassignFacesById(id, faceDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FaceApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final faceDto = FaceDto(); // FaceDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.reassignFacesById(id, faceDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FaceApi->reassignFacesById: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**faceDto** | [**FaceDto**](FaceDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**PersonResponseDto**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
15
mobile/openapi/doc/FaceDto.md
generated
Normal file
15
mobile/openapi/doc/FaceDto.md
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
# openapi.model.FaceDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
110
mobile/openapi/doc/PersonApi.md
generated
110
mobile/openapi/doc/PersonApi.md
generated
|
@ -9,16 +9,69 @@ All URIs are relative to */api*
|
|||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**createPerson**](PersonApi.md#createperson) | **POST** /person |
|
||||
[**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person |
|
||||
[**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} |
|
||||
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
[**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
|
||||
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
[**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
|
||||
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
|
||||
|
||||
# **createPerson**
|
||||
> PersonResponseDto createPerson()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = PersonApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.createPerson();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling PersonApi->createPerson: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**PersonResponseDto**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAllPeople**
|
||||
> PeopleResponseDto getAllPeople(withHidden)
|
||||
|
||||
|
@ -351,6 +404,63 @@ Name | Type | Description | Notes
|
|||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **reassignFaces**
|
||||
> List<PersonResponseDto> reassignFaces(id, assetFaceUpdateDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = PersonApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.reassignFaces(id, assetFaceUpdateDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling PersonApi->reassignFaces: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<PersonResponseDto>**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **updatePeople**
|
||||
> List<BulkIdResponseDto> updatePeople(peopleUpdateDto)
|
||||
|
||||
|
|
20
mobile/openapi/doc/PersonWithFacesResponseDto.md
generated
Normal file
20
mobile/openapi/doc/PersonWithFacesResponseDto.md
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
# openapi.model.PersonWithFacesResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**birthDate** | [**DateTime**](DateTime.md) | |
|
||||
**faces** | [**List<AssetFaceWithoutPersonResponseDto>**](AssetFaceWithoutPersonResponseDto.md) | | [default to const []]
|
||||
**id** | **String** | |
|
||||
**isHidden** | **bool** | |
|
||||
**name** | **String** | |
|
||||
**thumbnailPath** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
7
mobile/openapi/lib/api.dart
generated
7
mobile/openapi/lib/api.dart
generated
|
@ -34,6 +34,7 @@ part 'api/album_api.dart';
|
|||
part 'api/asset_api.dart';
|
||||
part 'api/audit_api.dart';
|
||||
part 'api/authentication_api.dart';
|
||||
part 'api/face_api.dart';
|
||||
part 'api/job_api.dart';
|
||||
part 'api/library_api.dart';
|
||||
part 'api/o_auth_api.dart';
|
||||
|
@ -63,6 +64,10 @@ part 'model/asset_bulk_upload_check_dto.dart';
|
|||
part 'model/asset_bulk_upload_check_item.dart';
|
||||
part 'model/asset_bulk_upload_check_response_dto.dart';
|
||||
part 'model/asset_bulk_upload_check_result.dart';
|
||||
part 'model/asset_face_response_dto.dart';
|
||||
part 'model/asset_face_update_dto.dart';
|
||||
part 'model/asset_face_update_item.dart';
|
||||
part 'model/asset_face_without_person_response_dto.dart';
|
||||
part 'model/asset_file_upload_response_dto.dart';
|
||||
part 'model/asset_ids_dto.dart';
|
||||
part 'model/asset_ids_response_dto.dart';
|
||||
|
@ -97,6 +102,7 @@ part 'model/download_info_dto.dart';
|
|||
part 'model/download_response_dto.dart';
|
||||
part 'model/entity_type.dart';
|
||||
part 'model/exif_response_dto.dart';
|
||||
part 'model/face_dto.dart';
|
||||
part 'model/file_checksum_dto.dart';
|
||||
part 'model/file_checksum_response_dto.dart';
|
||||
part 'model/file_report_dto.dart';
|
||||
|
@ -132,6 +138,7 @@ part 'model/people_update_item.dart';
|
|||
part 'model/person_response_dto.dart';
|
||||
part 'model/person_statistics_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/person_with_faces_response_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
|
|
122
mobile/openapi/lib/api/face_api.dart
generated
Normal file
122
mobile/openapi/lib/api/face_api.dart
generated
Normal file
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 FaceApi {
|
||||
FaceApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'GET /face' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getFacesWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/face';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<List<AssetFaceResponseDto>?> getFaces(String id,) async {
|
||||
final response = await getFacesWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetFaceResponseDto>') as List)
|
||||
.cast<AssetFaceResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /face/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [FaceDto] faceDto (required):
|
||||
Future<Response> reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/face/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = faceDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [FaceDto] faceDto (required):
|
||||
Future<PersonResponseDto?> reassignFacesById(String id, FaceDto faceDto,) async {
|
||||
final response = await reassignFacesByIdWithHttpInfo(id, faceDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
96
mobile/openapi/lib/api/person_api.dart
generated
96
mobile/openapi/lib/api/person_api.dart
generated
|
@ -16,6 +16,47 @@ class PersonApi {
|
|||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'POST /person' operation and returns the [Response].
|
||||
Future<Response> createPersonWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/person';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<PersonResponseDto?> createPerson() async {
|
||||
final response = await createPersonWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /person' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
@ -317,6 +358,61 @@ class PersonApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person/{id}/reassign' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
|
||||
Future<Response> reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/person/{id}/reassign'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = assetFaceUpdateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
|
||||
Future<List<PersonResponseDto>?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
|
||||
final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
|
||||
.cast<PersonResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
|
12
mobile/openapi/lib/api_client.dart
generated
12
mobile/openapi/lib/api_client.dart
generated
|
@ -215,6 +215,14 @@ class ApiClient {
|
|||
return AssetBulkUploadCheckResponseDto.fromJson(value);
|
||||
case 'AssetBulkUploadCheckResult':
|
||||
return AssetBulkUploadCheckResult.fromJson(value);
|
||||
case 'AssetFaceResponseDto':
|
||||
return AssetFaceResponseDto.fromJson(value);
|
||||
case 'AssetFaceUpdateDto':
|
||||
return AssetFaceUpdateDto.fromJson(value);
|
||||
case 'AssetFaceUpdateItem':
|
||||
return AssetFaceUpdateItem.fromJson(value);
|
||||
case 'AssetFaceWithoutPersonResponseDto':
|
||||
return AssetFaceWithoutPersonResponseDto.fromJson(value);
|
||||
case 'AssetFileUploadResponseDto':
|
||||
return AssetFileUploadResponseDto.fromJson(value);
|
||||
case 'AssetIdsDto':
|
||||
|
@ -283,6 +291,8 @@ class ApiClient {
|
|||
return EntityTypeTypeTransformer().decode(value);
|
||||
case 'ExifResponseDto':
|
||||
return ExifResponseDto.fromJson(value);
|
||||
case 'FaceDto':
|
||||
return FaceDto.fromJson(value);
|
||||
case 'FileChecksumDto':
|
||||
return FileChecksumDto.fromJson(value);
|
||||
case 'FileChecksumResponseDto':
|
||||
|
@ -353,6 +363,8 @@ class ApiClient {
|
|||
return PersonStatisticsResponseDto.fromJson(value);
|
||||
case 'PersonUpdateDto':
|
||||
return PersonUpdateDto.fromJson(value);
|
||||
case 'PersonWithFacesResponseDto':
|
||||
return PersonWithFacesResponseDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'ReactionLevel':
|
||||
|
|
158
mobile/openapi/lib/model/asset_face_response_dto.dart
generated
Normal file
158
mobile/openapi/lib/model/asset_face_response_dto.dart
generated
Normal file
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 AssetFaceResponseDto {
|
||||
/// Returns a new [AssetFaceResponseDto] instance.
|
||||
AssetFaceResponseDto({
|
||||
required this.boundingBoxX1,
|
||||
required this.boundingBoxX2,
|
||||
required this.boundingBoxY1,
|
||||
required this.boundingBoxY2,
|
||||
required this.id,
|
||||
required this.imageHeight,
|
||||
required this.imageWidth,
|
||||
required this.person,
|
||||
});
|
||||
|
||||
int boundingBoxX1;
|
||||
|
||||
int boundingBoxX2;
|
||||
|
||||
int boundingBoxY1;
|
||||
|
||||
int boundingBoxY2;
|
||||
|
||||
String id;
|
||||
|
||||
int imageHeight;
|
||||
|
||||
int imageWidth;
|
||||
|
||||
PersonResponseDto? person;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto &&
|
||||
other.boundingBoxX1 == boundingBoxX1 &&
|
||||
other.boundingBoxX2 == boundingBoxX2 &&
|
||||
other.boundingBoxY1 == boundingBoxY1 &&
|
||||
other.boundingBoxY2 == boundingBoxY2 &&
|
||||
other.id == id &&
|
||||
other.imageHeight == imageHeight &&
|
||||
other.imageWidth == imageWidth &&
|
||||
other.person == person;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(boundingBoxX1.hashCode) +
|
||||
(boundingBoxX2.hashCode) +
|
||||
(boundingBoxY1.hashCode) +
|
||||
(boundingBoxY2.hashCode) +
|
||||
(id.hashCode) +
|
||||
(imageHeight.hashCode) +
|
||||
(imageWidth.hashCode) +
|
||||
(person == null ? 0 : person!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'boundingBoxX1'] = this.boundingBoxX1;
|
||||
json[r'boundingBoxX2'] = this.boundingBoxX2;
|
||||
json[r'boundingBoxY1'] = this.boundingBoxY1;
|
||||
json[r'boundingBoxY2'] = this.boundingBoxY2;
|
||||
json[r'id'] = this.id;
|
||||
json[r'imageHeight'] = this.imageHeight;
|
||||
json[r'imageWidth'] = this.imageWidth;
|
||||
if (this.person != null) {
|
||||
json[r'person'] = this.person;
|
||||
} else {
|
||||
// json[r'person'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceResponseDto(
|
||||
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
|
||||
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
|
||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||
person: PersonResponseDto.fromJson(json[r'person']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'boundingBoxX1',
|
||||
'boundingBoxX2',
|
||||
'boundingBoxY1',
|
||||
'boundingBoxY2',
|
||||
'id',
|
||||
'imageHeight',
|
||||
'imageWidth',
|
||||
'person',
|
||||
};
|
||||
}
|
||||
|
98
mobile/openapi/lib/model/asset_face_update_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/asset_face_update_dto.dart
generated
Normal file
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 AssetFaceUpdateDto {
|
||||
/// Returns a new [AssetFaceUpdateDto] instance.
|
||||
AssetFaceUpdateDto({
|
||||
this.data = const [],
|
||||
});
|
||||
|
||||
List<AssetFaceUpdateItem> data;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateDto &&
|
||||
other.data == data;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(data.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceUpdateDto[data=$data]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'data'] = this.data;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceUpdateDto(
|
||||
data: AssetFaceUpdateItem.listFromJson(json[r'data']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'data',
|
||||
};
|
||||
}
|
||||
|
106
mobile/openapi/lib/model/asset_face_update_item.dart
generated
Normal file
106
mobile/openapi/lib/model/asset_face_update_item.dart
generated
Normal file
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 AssetFaceUpdateItem {
|
||||
/// Returns a new [AssetFaceUpdateItem] instance.
|
||||
AssetFaceUpdateItem({
|
||||
required this.assetId,
|
||||
required this.personId,
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
String personId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateItem &&
|
||||
other.assetId == assetId &&
|
||||
other.personId == personId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(personId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceUpdateItem[assetId=$assetId, personId=$personId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'personId'] = this.personId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceUpdateItem] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateItem? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceUpdateItem(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
personId: mapValueOfType<String>(json, r'personId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceUpdateItem> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceUpdateItem>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceUpdateItem.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceUpdateItem> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceUpdateItem>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceUpdateItem.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceUpdateItem-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceUpdateItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceUpdateItem>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceUpdateItem.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'personId',
|
||||
};
|
||||
}
|
||||
|
146
mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
generated
Normal file
146
mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
generated
Normal file
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 AssetFaceWithoutPersonResponseDto {
|
||||
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance.
|
||||
AssetFaceWithoutPersonResponseDto({
|
||||
required this.boundingBoxX1,
|
||||
required this.boundingBoxX2,
|
||||
required this.boundingBoxY1,
|
||||
required this.boundingBoxY2,
|
||||
required this.id,
|
||||
required this.imageHeight,
|
||||
required this.imageWidth,
|
||||
});
|
||||
|
||||
int boundingBoxX1;
|
||||
|
||||
int boundingBoxX2;
|
||||
|
||||
int boundingBoxY1;
|
||||
|
||||
int boundingBoxY2;
|
||||
|
||||
String id;
|
||||
|
||||
int imageHeight;
|
||||
|
||||
int imageWidth;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
|
||||
other.boundingBoxX1 == boundingBoxX1 &&
|
||||
other.boundingBoxX2 == boundingBoxX2 &&
|
||||
other.boundingBoxY1 == boundingBoxY1 &&
|
||||
other.boundingBoxY2 == boundingBoxY2 &&
|
||||
other.id == id &&
|
||||
other.imageHeight == imageHeight &&
|
||||
other.imageWidth == imageWidth;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(boundingBoxX1.hashCode) +
|
||||
(boundingBoxX2.hashCode) +
|
||||
(boundingBoxY1.hashCode) +
|
||||
(boundingBoxY2.hashCode) +
|
||||
(id.hashCode) +
|
||||
(imageHeight.hashCode) +
|
||||
(imageWidth.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'boundingBoxX1'] = this.boundingBoxX1;
|
||||
json[r'boundingBoxX2'] = this.boundingBoxX2;
|
||||
json[r'boundingBoxY1'] = this.boundingBoxY1;
|
||||
json[r'boundingBoxY2'] = this.boundingBoxY2;
|
||||
json[r'id'] = this.id;
|
||||
json[r'imageHeight'] = this.imageHeight;
|
||||
json[r'imageWidth'] = this.imageWidth;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceWithoutPersonResponseDto(
|
||||
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
|
||||
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
|
||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceWithoutPersonResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceWithoutPersonResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceWithoutPersonResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceWithoutPersonResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceWithoutPersonResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceWithoutPersonResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceWithoutPersonResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceWithoutPersonResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceWithoutPersonResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceWithoutPersonResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'boundingBoxX1',
|
||||
'boundingBoxX2',
|
||||
'boundingBoxY1',
|
||||
'boundingBoxY2',
|
||||
'id',
|
||||
'imageHeight',
|
||||
'imageWidth',
|
||||
};
|
||||
}
|
||||
|
4
mobile/openapi/lib/model/asset_response_dto.dart
generated
4
mobile/openapi/lib/model/asset_response_dto.dart
generated
|
@ -104,7 +104,7 @@ class AssetResponseDto {
|
|||
|
||||
String ownerId;
|
||||
|
||||
List<PersonResponseDto> people;
|
||||
List<PersonWithFacesResponseDto> people;
|
||||
|
||||
bool resized;
|
||||
|
||||
|
@ -299,7 +299,7 @@ class AssetResponseDto {
|
|||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||
owner: UserResponseDto.fromJson(json[r'owner']),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
people: PersonResponseDto.listFromJson(json[r'people']),
|
||||
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
|
||||
resized: mapValueOfType<bool>(json, r'resized')!,
|
||||
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
|
||||
stack: AssetResponseDto.listFromJson(json[r'stack']),
|
||||
|
|
98
mobile/openapi/lib/model/face_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/face_dto.dart
generated
Normal file
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 FaceDto {
|
||||
/// Returns a new [FaceDto] instance.
|
||||
FaceDto({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is FaceDto &&
|
||||
other.id == id;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'FaceDto[id=$id]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [FaceDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static FaceDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return FaceDto(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<FaceDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <FaceDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = FaceDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, FaceDto> mapFromJson(dynamic json) {
|
||||
final map = <String, FaceDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = FaceDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of FaceDto-objects as value to a dart map
|
||||
static Map<String, List<FaceDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<FaceDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = FaceDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
3
mobile/openapi/lib/model/job_command.dart
generated
3
mobile/openapi/lib/model/job_command.dart
generated
|
@ -27,6 +27,7 @@ class JobCommand {
|
|||
static const pause = JobCommand._(r'pause');
|
||||
static const resume = JobCommand._(r'resume');
|
||||
static const empty = JobCommand._(r'empty');
|
||||
static const clearFailed = JobCommand._(r'clear-failed');
|
||||
|
||||
/// List of all possible values in this [enum][JobCommand].
|
||||
static const values = <JobCommand>[
|
||||
|
@ -34,6 +35,7 @@ class JobCommand {
|
|||
pause,
|
||||
resume,
|
||||
empty,
|
||||
clearFailed,
|
||||
];
|
||||
|
||||
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
|
||||
|
@ -76,6 +78,7 @@ class JobCommandTypeTransformer {
|
|||
case r'pause': return JobCommand.pause;
|
||||
case r'resume': return JobCommand.resume;
|
||||
case r'empty': return JobCommand.empty;
|
||||
case r'clear-failed': return JobCommand.clearFailed;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
|
142
mobile/openapi/lib/model/person_with_faces_response_dto.dart
generated
Normal file
142
mobile/openapi/lib/model/person_with_faces_response_dto.dart
generated
Normal file
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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 PersonWithFacesResponseDto {
|
||||
/// Returns a new [PersonWithFacesResponseDto] instance.
|
||||
PersonWithFacesResponseDto({
|
||||
required this.birthDate,
|
||||
this.faces = const [],
|
||||
required this.id,
|
||||
required this.isHidden,
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
|
||||
List<AssetFaceWithoutPersonResponseDto> faces;
|
||||
|
||||
String id;
|
||||
|
||||
bool isHidden;
|
||||
|
||||
String name;
|
||||
|
||||
String thumbnailPath;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.faces == faces &&
|
||||
other.id == id &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(faces.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.birthDate != null) {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
json[r'faces'] = this.faces;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isHidden'] = this.isHidden;
|
||||
json[r'name'] = this.name;
|
||||
json[r'thumbnailPath'] = this.thumbnailPath;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PersonWithFacesResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PersonWithFacesResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PersonWithFacesResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', ''),
|
||||
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PersonWithFacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PersonWithFacesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PersonWithFacesResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PersonWithFacesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PersonWithFacesResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PersonWithFacesResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PersonWithFacesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PersonWithFacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PersonWithFacesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PersonWithFacesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'birthDate',
|
||||
'faces',
|
||||
'id',
|
||||
'isHidden',
|
||||
'name',
|
||||
'thumbnailPath',
|
||||
};
|
||||
}
|
||||
|
62
mobile/openapi/test/asset_face_response_dto_test.dart
generated
Normal file
62
mobile/openapi/test/asset_face_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceResponseDto
|
||||
void main() {
|
||||
// final instance = AssetFaceResponseDto();
|
||||
|
||||
group('test AssetFaceResponseDto', () {
|
||||
// int boundingBoxX1
|
||||
test('to test the property `boundingBoxX1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxX2
|
||||
test('to test the property `boundingBoxX2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY1
|
||||
test('to test the property `boundingBoxY1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY2
|
||||
test('to test the property `boundingBoxY2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageHeight
|
||||
test('to test the property `imageHeight`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageWidth
|
||||
test('to test the property `imageWidth`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// PersonResponseDto person
|
||||
test('to test the property `person`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
27
mobile/openapi/test/asset_face_update_dto_test.dart
generated
Normal file
27
mobile/openapi/test/asset_face_update_dto_test.dart
generated
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceUpdateDto
|
||||
void main() {
|
||||
// final instance = AssetFaceUpdateDto();
|
||||
|
||||
group('test AssetFaceUpdateDto', () {
|
||||
// List<AssetFaceUpdateItem> data (default value: const [])
|
||||
test('to test the property `data`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
32
mobile/openapi/test/asset_face_update_item_test.dart
generated
Normal file
32
mobile/openapi/test/asset_face_update_item_test.dart
generated
Normal file
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceUpdateItem
|
||||
void main() {
|
||||
// final instance = AssetFaceUpdateItem();
|
||||
|
||||
group('test AssetFaceUpdateItem', () {
|
||||
// String assetId
|
||||
test('to test the property `assetId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String personId
|
||||
test('to test the property `personId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
57
mobile/openapi/test/asset_face_without_person_response_dto_test.dart
generated
Normal file
57
mobile/openapi/test/asset_face_without_person_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceWithoutPersonResponseDto
|
||||
void main() {
|
||||
// final instance = AssetFaceWithoutPersonResponseDto();
|
||||
|
||||
group('test AssetFaceWithoutPersonResponseDto', () {
|
||||
// int boundingBoxX1
|
||||
test('to test the property `boundingBoxX1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxX2
|
||||
test('to test the property `boundingBoxX2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY1
|
||||
test('to test the property `boundingBoxY1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY2
|
||||
test('to test the property `boundingBoxY2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageHeight
|
||||
test('to test the property `imageHeight`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageWidth
|
||||
test('to test the property `imageWidth`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
2
mobile/openapi/test/asset_response_dto_test.dart
generated
2
mobile/openapi/test/asset_response_dto_test.dart
generated
|
@ -127,7 +127,7 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
// List<PersonResponseDto> people (default value: const [])
|
||||
// List<PersonWithFacesResponseDto> people (default value: const [])
|
||||
test('to test the property `people`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
|
31
mobile/openapi/test/face_api_test.dart
generated
Normal file
31
mobile/openapi/test/face_api_test.dart
generated
Normal file
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
||||
/// tests for FaceApi
|
||||
void main() {
|
||||
// final instance = FaceApi();
|
||||
|
||||
group('tests for FaceApi', () {
|
||||
//Future<List<AssetFaceResponseDto>> getFaces(String id) async
|
||||
test('test getFaces', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<PersonResponseDto> reassignFacesById(String id, FaceDto faceDto) async
|
||||
test('test reassignFacesById', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
});
|
||||
}
|
27
mobile/openapi/test/face_dto_test.dart
generated
Normal file
27
mobile/openapi/test/face_dto_test.dart
generated
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for FaceDto
|
||||
void main() {
|
||||
// final instance = FaceDto();
|
||||
|
||||
group('test FaceDto', () {
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
10
mobile/openapi/test/person_api_test.dart
generated
10
mobile/openapi/test/person_api_test.dart
generated
|
@ -17,6 +17,11 @@ void main() {
|
|||
// final instance = PersonApi();
|
||||
|
||||
group('tests for PersonApi', () {
|
||||
//Future<PersonResponseDto> createPerson() async
|
||||
test('test createPerson', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<PeopleResponseDto> getAllPeople({ bool withHidden }) async
|
||||
test('test getAllPeople', () async {
|
||||
// TODO
|
||||
|
@ -47,6 +52,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<PersonResponseDto>> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto) async
|
||||
test('test reassignFaces', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
|
||||
test('test updatePeople', () async {
|
||||
// TODO
|
||||
|
|
52
mobile/openapi/test/person_with_faces_response_dto_test.dart
generated
Normal file
52
mobile/openapi/test/person_with_faces_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// 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
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for PersonWithFacesResponseDto
|
||||
void main() {
|
||||
// final instance = PersonWithFacesResponseDto();
|
||||
|
||||
group('test PersonWithFacesResponseDto', () {
|
||||
// DateTime birthDate
|
||||
test('to test the property `birthDate`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<AssetFaceWithoutPersonResponseDto> faces (default value: const [])
|
||||
test('to test the property `faces`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isHidden
|
||||
test('to test the property `isHidden`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String name
|
||||
test('to test the property `name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String thumbnailPath
|
||||
test('to test the property `thumbnailPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
131
mobile/test/asset_extensions_test.dart
Normal file
131
mobile/test/asset_extensions_test.dart
Normal file
|
@ -0,0 +1,131 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
ExifInfo makeExif({
|
||||
DateTime? dateTimeOriginal,
|
||||
String? timeZone,
|
||||
}) {
|
||||
return ExifInfo(
|
||||
dateTimeOriginal: dateTimeOriginal,
|
||||
timeZone: timeZone,
|
||||
);
|
||||
}
|
||||
|
||||
Asset makeAsset({
|
||||
required String id,
|
||||
required DateTime createdAt,
|
||||
ExifInfo? exifInfo,
|
||||
}) {
|
||||
return Asset(
|
||||
checksum: '',
|
||||
localId: id,
|
||||
remoteId: id,
|
||||
ownerId: 1,
|
||||
fileCreatedAt: createdAt,
|
||||
fileModifiedAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: id,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
stackCount: 0,
|
||||
exifInfo: exifInfo,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Init Timezone DB
|
||||
initializeTimeZones();
|
||||
|
||||
group("Returns local time and offset if no exifInfo", () {
|
||||
test('returns createdAt directly if in local', () {
|
||||
final createdAt = DateTime(2023, 12, 12, 12, 12, 12);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
expect(dt, createdAt);
|
||||
expect(tz, createdAt.timeZoneOffset);
|
||||
});
|
||||
|
||||
test('returns createdAt in local if in utc', () {
|
||||
final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final localCreatedAt = createdAt.toLocal();
|
||||
expect(dt, localCreatedAt);
|
||||
expect(tz, localCreatedAt.timeZoneOffset);
|
||||
});
|
||||
});
|
||||
|
||||
group("Returns dateTimeOriginal", () {
|
||||
test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dt, dateTimeInUTC);
|
||||
expect(tz, dateTimeInUTC.timeZoneOffset);
|
||||
});
|
||||
|
||||
test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone',
|
||||
() {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
final e = makeExif(
|
||||
dateTimeOriginal: dateTimeOriginal,
|
||||
timeZone: "#_#",
|
||||
); // Invalid timezone
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dt, dateTimeInUTC);
|
||||
expect(tz, dateTimeInUTC.timeZoneOffset);
|
||||
});
|
||||
});
|
||||
|
||||
group("Returns adjusted time if timezone available", () {
|
||||
test('With timezone as location', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
const location = "Asia/Hong_Kong";
|
||||
final e =
|
||||
makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final adjustedTime =
|
||||
TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location));
|
||||
expect(dt, adjustedTime);
|
||||
expect(tz, adjustedTime.timeZoneOffset);
|
||||
});
|
||||
|
||||
test('With timezone as offset', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
const offset = "utc+08:00";
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final location = getLocation("Asia/Hong_Kong");
|
||||
final offsetFromLocation =
|
||||
Duration(milliseconds: location.currentTimeZone.offset);
|
||||
final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation);
|
||||
|
||||
// Adds the offset to the actual time and returns the offset separately
|
||||
expect(dt, adjustedTime);
|
||||
expect(tz, offsetFromLocation);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -62,6 +62,9 @@
|
|||
"versioning": "node"
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"mobile/openapi/pubspec.yaml"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"http",
|
||||
"latlong2",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20231130@sha256:2f3b4bc0b50a0710e4a0867b4842ebde3a709d18fd19b095b8bfb884082cfa18 as dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20231201@sha256:4701c0c5920c78e73040dd2b74d22042ffce393f1a9d3453d90a0ecf81ff8649 as dev
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
|
@ -23,7 +23,7 @@ RUN npm run build
|
|||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20231130@sha256:dd91bfac4090357605a862823a99b50cf01cbc519723198f7aebb6b0517fab1d
|
||||
FROM ghcr.io/immich-app/base-server-prod:20231201@sha256:b8e86cf4c3cad872f54bab25a83f7503480049eea5c0ae36a8b8460b13cad3b5
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production
|
||||
|
|
|
@ -3220,6 +3220,103 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/face": {
|
||||
"get": {
|
||||
"operationId": "getFaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Face"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/face/{id}": {
|
||||
"put": {
|
||||
"operationId": "reassignFacesById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FaceDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Face"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/jobs": {
|
||||
"get": {
|
||||
"operationId": "getAllJobsStatus",
|
||||
|
@ -4022,6 +4119,36 @@
|
|||
"Person"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createPerson",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Person"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updatePeople",
|
||||
"parameters": [],
|
||||
|
@ -4258,6 +4385,61 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/person/{id}/reassign": {
|
||||
"put": {
|
||||
"operationId": "reassignFaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetFaceUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Person"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/person/{id}/statistics": {
|
||||
"get": {
|
||||
"operationId": "getPersonStatistics",
|
||||
|
@ -6557,6 +6739,118 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceResponseDto": {
|
||||
"properties": {
|
||||
"boundingBoxX1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxX2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"imageHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"imageWidth": {
|
||||
"type": "integer"
|
||||
},
|
||||
"person": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"imageHeight",
|
||||
"imageWidth",
|
||||
"boundingBoxX1",
|
||||
"boundingBoxX2",
|
||||
"boundingBoxY1",
|
||||
"boundingBoxY2",
|
||||
"person"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceUpdateDto": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceUpdateItem"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceUpdateItem": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"personId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"personId",
|
||||
"assetId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceWithoutPersonResponseDto": {
|
||||
"properties": {
|
||||
"boundingBoxX1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxX2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"imageHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"imageWidth": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"imageHeight",
|
||||
"imageWidth",
|
||||
"boundingBoxX1",
|
||||
"boundingBoxX2",
|
||||
"boundingBoxY1",
|
||||
"boundingBoxY2"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFileUploadResponseDto": {
|
||||
"properties": {
|
||||
"duplicate": {
|
||||
|
@ -6719,7 +7013,7 @@
|
|||
},
|
||||
"people": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
"$ref": "#/components/schemas/PersonWithFacesResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
@ -7452,6 +7746,18 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FaceDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileChecksumDto": {
|
||||
"properties": {
|
||||
"filenames": {
|
||||
|
@ -7548,7 +7854,8 @@
|
|||
"start",
|
||||
"pause",
|
||||
"resume",
|
||||
"empty"
|
||||
"empty",
|
||||
"clear-failed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -8146,6 +8453,42 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PersonWithFacesResponseDto": {
|
||||
"properties": {
|
||||
"birthDate": {
|
||||
"format": "date",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"faces": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnailPath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"birthDate",
|
||||
"faces",
|
||||
"id",
|
||||
"name",
|
||||
"thumbnailPath",
|
||||
"isHidden"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
|
|
|
@ -41,6 +41,8 @@ export enum Permission {
|
|||
PERSON_READ = 'person.read',
|
||||
PERSON_WRITE = 'person.write',
|
||||
PERSON_MERGE = 'person.merge',
|
||||
PERSON_CREATE = 'person.create',
|
||||
PERSON_REASSIGN = 'person.reassign',
|
||||
|
||||
PARTNER_UPDATE = 'partner.update',
|
||||
}
|
||||
|
@ -247,6 +249,12 @@ export class AccessCore {
|
|||
case Permission.PERSON_MERGE:
|
||||
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_CREATE:
|
||||
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_REASSIGN:
|
||||
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PARTNER_UPDATE:
|
||||
return await this.repository.partner.checkUpdateAccess(authUser.id, ids);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
import { Optional, ValidateUUID } from '../../domain.util';
|
||||
|
||||
export class CreateAlbumDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
albumName!: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AssetEntity, AssetType } from '@app/infra/entities';
|
||||
import { AssetEntity, AssetFaceEntity, AssetType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PersonResponseDto, mapFace } from '../../person/person.dto';
|
||||
import { PersonWithFacesResponseDto } from '../../person/person.dto';
|
||||
import { TagResponseDto, mapTag } from '../../tag';
|
||||
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
|
||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||
|
@ -39,7 +39,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
exifInfo?: ExifResponseDto;
|
||||
smartInfo?: SmartInfoResponseDto;
|
||||
tags?: TagResponseDto[];
|
||||
people?: PersonResponseDto[];
|
||||
people?: PersonWithFacesResponseDto[];
|
||||
/**base64 encoded sha1 hash */
|
||||
checksum!: string;
|
||||
stackParentId?: string | null;
|
||||
|
@ -53,6 +53,24 @@ export type AssetMapOptions = {
|
|||
withStack?: boolean;
|
||||
};
|
||||
|
||||
const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
|
||||
const result: PersonWithFacesResponseDto[] = [];
|
||||
if (faces) {
|
||||
faces.forEach((face) => {
|
||||
if (face.person) {
|
||||
const existingPersonEntry = result.find((item) => item.id === face.person!.id);
|
||||
if (existingPersonEntry) {
|
||||
existingPersonEntry.faces.push(face);
|
||||
} else {
|
||||
result.push({ ...face.person!, faces: [face] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
|
||||
|
@ -96,16 +114,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
|||
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
tags: entity.tags?.map(mapTag),
|
||||
people: entity.faces
|
||||
?.map(mapFace)
|
||||
.filter((person): person is PersonResponseDto => person !== null)
|
||||
.reduce((people, person) => {
|
||||
const existingPerson = people.find((p) => p.id === person.id);
|
||||
if (!existingPerson) {
|
||||
people.push(person);
|
||||
}
|
||||
return people;
|
||||
}, [] as PersonResponseDto[]),
|
||||
people: peopleWithFaces(entity.faces),
|
||||
checksum: entity.checksum.toString('base64'),
|
||||
stackParentId: entity.stackParentId,
|
||||
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
|
||||
|
|
|
@ -18,6 +18,7 @@ export enum JobCommand {
|
|||
PAUSE = 'pause',
|
||||
RESUME = 'resume',
|
||||
EMPTY = 'empty',
|
||||
CLEAR_FAILED = 'clear-failed',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
ISystemConfigRepository,
|
||||
JobHandler,
|
||||
JobItem,
|
||||
QueueCleanType,
|
||||
} from '../repositories';
|
||||
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { JobCommand, JobName, QueueName } from './job.constants';
|
||||
|
@ -49,6 +50,11 @@ export class JobService {
|
|||
case JobCommand.EMPTY:
|
||||
await this.jobRepository.empty(queueName);
|
||||
break;
|
||||
|
||||
case JobCommand.CLEAR_FAILED:
|
||||
const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED);
|
||||
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.getJobStatus(queueName);
|
||||
|
@ -195,7 +201,7 @@ export class JobService {
|
|||
const { id } = item.data;
|
||||
const person = await this.personRepository.getById(id);
|
||||
if (person) {
|
||||
this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, id);
|
||||
this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -223,7 +229,9 @@ export class JobService {
|
|||
}
|
||||
|
||||
const [asset] = await this.assetRepository.getByIds([item.data.id]);
|
||||
if (asset) {
|
||||
|
||||
// Only live-photo motion part will be marked as not visible immediately on upload. Skip notifying clients
|
||||
if (asset && asset.isVisible) {
|
||||
this.communicationRepository.send(CommunicationEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,16 @@ import {
|
|||
assetStub,
|
||||
newAlbumRepositoryMock,
|
||||
newAssetRepositoryMock,
|
||||
newCommunicationRepositoryMock,
|
||||
newCryptoRepositoryMock,
|
||||
newJobRepositoryMock,
|
||||
newMediaRepositoryMock,
|
||||
newMetadataRepositoryMock,
|
||||
newMoveRepositoryMock,
|
||||
newPersonRepositoryMock,
|
||||
newStorageRepositoryMock,
|
||||
newSystemConfigRepositoryMock,
|
||||
probeStub,
|
||||
} from '@test';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Stats } from 'fs';
|
||||
|
@ -17,10 +20,13 @@ import { constants } from 'fs/promises';
|
|||
import { when } from 'jest-when';
|
||||
import { JobName } from '../job';
|
||||
import {
|
||||
CommunicationEvent,
|
||||
IAlbumRepository,
|
||||
IAssetRepository,
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
IMediaRepository,
|
||||
IMetadataRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
|
@ -30,7 +36,7 @@ import {
|
|||
WithProperty,
|
||||
WithoutProperty,
|
||||
} from '../repositories';
|
||||
import { MetadataService } from './metadata.service';
|
||||
import { MetadataService, Orientation } from './metadata.service';
|
||||
|
||||
describe(MetadataService.name, () => {
|
||||
let albumMock: jest.Mocked<IAlbumRepository>;
|
||||
|
@ -40,8 +46,10 @@ describe(MetadataService.name, () => {
|
|||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let metadataMock: jest.Mocked<IMetadataRepository>;
|
||||
let moveMock: jest.Mocked<IMoveRepository>;
|
||||
let mediaMock: jest.Mocked<IMediaRepository>;
|
||||
let personMock: jest.Mocked<IPersonRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
let communicationMock: jest.Mocked<ICommunicationRepository>;
|
||||
let sut: MetadataService;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -53,7 +61,9 @@ describe(MetadataService.name, () => {
|
|||
metadataMock = newMetadataRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
communicationMock = newCommunicationRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
mediaMock = newMediaRepositoryMock();
|
||||
|
||||
sut = new MetadataService(
|
||||
albumMock,
|
||||
|
@ -63,7 +73,9 @@ describe(MetadataService.name, () => {
|
|||
metadataMock,
|
||||
storageMock,
|
||||
configMock,
|
||||
mediaMock,
|
||||
moveMock,
|
||||
communicationMock,
|
||||
personMock,
|
||||
);
|
||||
});
|
||||
|
@ -166,6 +178,23 @@ describe(MetadataService.name, () => {
|
|||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
});
|
||||
|
||||
it('should notify clients on live photo link', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true);
|
||||
expect(communicationMock.send).toHaveBeenCalledWith(
|
||||
CommunicationEvent.ASSET_HIDDEN,
|
||||
assetStub.livePhotoMotionAsset.ownerId,
|
||||
assetStub.livePhotoMotionAsset.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueMetadataExtraction', () => {
|
||||
|
@ -277,6 +306,7 @@ describe(MetadataService.name, () => {
|
|||
|
||||
it('should not apply motion photos if asset is video', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
||||
|
@ -287,6 +317,19 @@ describe(MetadataService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should extract the correct video orientation', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
metadataMock.readTags.mockResolvedValue(null);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ orientation: Orientation.Rotate270CW }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply motion photos', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
||||
metadataMock.readTags.mockResolvedValue({
|
||||
|
|
|
@ -9,11 +9,14 @@ import { Subscription } from 'rxjs';
|
|||
import { usePagination } from '../domain.util';
|
||||
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||
import {
|
||||
CommunicationEvent,
|
||||
ExifDuration,
|
||||
IAlbumRepository,
|
||||
IAssetRepository,
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
IMediaRepository,
|
||||
IMetadataRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
|
@ -49,6 +52,17 @@ interface DirectoryEntry {
|
|||
Item: DirectoryItem;
|
||||
}
|
||||
|
||||
export enum Orientation {
|
||||
Horizontal = '1',
|
||||
MirrorHorizontal = '2',
|
||||
Rotate180 = '3',
|
||||
MirrorVertical = '4',
|
||||
MirrorHorizontalRotate270CW = '5',
|
||||
Rotate90CW = '6',
|
||||
MirrorHorizontalRotate90CW = '7',
|
||||
Rotate270CW = '8',
|
||||
}
|
||||
|
||||
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<
|
||||
ExifEntity,
|
||||
'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn'
|
||||
|
@ -90,7 +104,9 @@ export class MetadataService {
|
|||
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
|
@ -154,6 +170,9 @@ export class MetadataService {
|
|||
await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
|
||||
await this.albumRepository.removeAsset(motionAsset.id);
|
||||
|
||||
// Notify clients to hide the linked live photo asset
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -182,6 +201,27 @@ export class MetadataService {
|
|||
|
||||
const { exifData, tags } = await this.exifData(asset);
|
||||
|
||||
if (asset.type === AssetType.VIDEO) {
|
||||
const { videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||
|
||||
if (videoStreams[0]) {
|
||||
switch (videoStreams[0].rotation) {
|
||||
case -90:
|
||||
exifData.orientation = Orientation.Rotate90CW;
|
||||
break;
|
||||
case 0:
|
||||
exifData.orientation = Orientation.Horizontal;
|
||||
break;
|
||||
case 90:
|
||||
exifData.orientation = Orientation.Rotate270CW;
|
||||
break;
|
||||
case 180:
|
||||
exifData.orientation = Orientation.Rotate180;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.applyMotionPhotos(asset, tags);
|
||||
await this.applyReverseGeocoding(asset, exifData);
|
||||
await this.assetRepository.upsertExif(exifData);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { Optional, ValidateUUID, toBoolean } from '../domain.util';
|
||||
|
||||
export class PersonUpdateDto {
|
||||
|
@ -73,6 +74,51 @@ export class PersonResponseDto {
|
|||
isHidden!: boolean;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
faces!: AssetFaceWithoutPersonResponseDto[];
|
||||
}
|
||||
|
||||
export class AssetFaceWithoutPersonResponseDto {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageHeight!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageWidth!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX2!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY2!: number;
|
||||
}
|
||||
|
||||
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
|
||||
person!: PersonResponseDto | null;
|
||||
}
|
||||
|
||||
export class AssetFaceUpdateDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetFaceUpdateItem)
|
||||
data!: AssetFaceUpdateItem[];
|
||||
}
|
||||
|
||||
export class FaceDto {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class AssetFaceUpdateItem {
|
||||
@ValidateUUID()
|
||||
personId!: string;
|
||||
|
||||
@ValidateUUID()
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
export class PersonStatisticsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
assets!: number;
|
||||
|
@ -98,10 +144,15 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapFace(face: AssetFaceEntity): PersonResponseDto | null {
|
||||
if (face.person) {
|
||||
return mapPerson(face.person);
|
||||
}
|
||||
|
||||
return null;
|
||||
export function mapFaces(face: AssetFaceEntity, authUser: AuthUserDto): AssetFaceResponseDto {
|
||||
return {
|
||||
id: face.id,
|
||||
imageHeight: face.imageHeight,
|
||||
imageWidth: face.imageWidth,
|
||||
boundingBoxX1: face.boundingBoxX1,
|
||||
boundingBoxX2: face.boundingBoxX2,
|
||||
boundingBoxY1: face.boundingBoxY1,
|
||||
boundingBoxY2: face.boundingBoxY2,
|
||||
person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
ISystemConfigRepository,
|
||||
WithoutProperty,
|
||||
} from '../repositories';
|
||||
import { PersonResponseDto } from './person.dto';
|
||||
import { PersonResponseDto, mapFaces } from './person.dto';
|
||||
import { PersonService } from './person.service';
|
||||
|
||||
const responseDto: PersonResponseDto = {
|
||||
|
@ -339,7 +339,7 @@ describe(PersonService.name, () => {
|
|||
).resolves.toEqual(responseDto);
|
||||
|
||||
expect(personMock.getById).toHaveBeenCalledWith('person-1');
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.assetId });
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id });
|
||||
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
||||
{
|
||||
assetId: faceStub.face1.assetId,
|
||||
|
@ -375,6 +375,139 @@ describe(PersonService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('reassignFaces', () => {
|
||||
it('should throw an error if user has no access to the person', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: 'asset-face-1', assetId: '' }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
it('should reassign a face', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id]));
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
|
||||
}),
|
||||
).resolves.toEqual([personStub.noName]);
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonMigration', () => {
|
||||
it('should not move person files', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFacesById', () => {
|
||||
it('should get the bounding boxes for an asset', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId]));
|
||||
personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]);
|
||||
await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([
|
||||
mapFaces(faceStub.primaryFace1, authStub.admin),
|
||||
]);
|
||||
});
|
||||
it('should reject if the user has not access to the asset', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]);
|
||||
await expect(sut.getFacesById(authStub.admin, { id: faceStub.primaryFace1.assetId })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewFeaturePhoto', () => {
|
||||
it('should change person feature photo', async () => {
|
||||
personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
await sut.createNewFeaturePhoto([personStub.newThumbnail.id]);
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reassignFacesById', () => {
|
||||
it('should create a new person', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
birthDate: personStub.noName.birthDate,
|
||||
isHidden: personStub.noName.isHidden,
|
||||
id: personStub.noName.id,
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should fail if user has not the correct permissions on the asset', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set());
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPerson', () => {
|
||||
it('should create a new person', async () => {
|
||||
personMock.create.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
|
||||
await expect(sut.createPerson(authStub.admin)).resolves.toBe(personStub.primaryPerson);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonDelete', () => {
|
||||
it('should stop if a person has not be found', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(false);
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(storageMock.unlink).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should delete a person', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
|
||||
await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(true);
|
||||
expect(personMock.delete).toHaveBeenCalledWith(personStub.primaryPerson);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(personStub.primaryPerson.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonCleanup', () => {
|
||||
it('should delete people without faces', async () => {
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);
|
||||
|
@ -515,6 +648,7 @@ describe(PersonService.name, () => {
|
|||
searchMock.searchFaces.mockResolvedValue(faceSearch.oneRemoteMatch);
|
||||
personMock.create.mockResolvedValue(personStub.noName);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
personMock.createFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: assetStub.image.id });
|
||||
|
||||
|
@ -557,16 +691,16 @@ describe(PersonService.name, () => {
|
|||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip an person with a face asset id not found', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
it('should skip a person with a face asset id not found', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.id });
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip a person with a face asset id without a thumbnail', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
|
@ -574,7 +708,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.middle]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -601,7 +735,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail without going negative', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.start]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.start);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -622,7 +756,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail without overflowing', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.end]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.end);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -646,15 +780,12 @@ describe(PersonService.name, () => {
|
|||
it('should require person.write and person.merge permission', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([]);
|
||||
personMock.delete.mockResolvedValue(personStub.mergePerson);
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(personMock.prepareReassignFaces).not.toHaveBeenCalled();
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
|
||||
expect(personMock.delete).not.toHaveBeenCalled();
|
||||
|
@ -664,7 +795,6 @@ describe(PersonService.name, () => {
|
|||
it('should merge two people', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([]);
|
||||
personMock.delete.mockResolvedValue(personStub.mergePerson);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
@ -673,11 +803,6 @@ describe(PersonService.name, () => {
|
|||
{ id: 'person-2', success: true },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
});
|
||||
|
||||
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
|
@ -690,29 +815,6 @@ describe(PersonService.name, () => {
|
|||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should delete conflicting faces before merging', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValue(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
|
||||
{ id: 'person-2', success: true },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEARCH_REMOVE_FACE,
|
||||
data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id },
|
||||
});
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should throw an error when the primary person is not found', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
@ -735,7 +837,6 @@ describe(PersonService.name, () => {
|
|||
{ id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.delete).not.toHaveBeenCalled();
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
|
@ -744,7 +845,6 @@ describe(PersonService.name, () => {
|
|||
it('should handle an error reassigning faces', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValue(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
|
||||
personMock.reassignFaces.mockRejectedValue(new Error('update failed'));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
|
|
@ -28,6 +28,9 @@ import {
|
|||
import { StorageCore } from '../storage';
|
||||
import { SystemConfigCore } from '../system-config';
|
||||
import {
|
||||
AssetFaceResponseDto,
|
||||
AssetFaceUpdateDto,
|
||||
FaceDto,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
|
@ -35,6 +38,7 @@ import {
|
|||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonUpdateDto,
|
||||
mapFaces,
|
||||
mapPerson,
|
||||
} from './person.dto';
|
||||
|
||||
|
@ -80,6 +84,86 @@ export class PersonService {
|
|||
};
|
||||
}
|
||||
|
||||
createPerson(authUser: AuthUserDto): Promise<PersonResponseDto> {
|
||||
return this.repository.create({ ownerId: authUser.id });
|
||||
}
|
||||
|
||||
async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
|
||||
const person = await this.findOrFail(personId);
|
||||
const result: PersonResponseDto[] = [];
|
||||
const changeFeaturePhoto: string[] = [];
|
||||
for (const data of dto.data) {
|
||||
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
||||
|
||||
for (const face of faces) {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_CREATE, face.id);
|
||||
if (person.faceAssetId === null) {
|
||||
changeFeaturePhoto.push(person.id);
|
||||
}
|
||||
if (face.person && face.person.faceAssetId === face.id) {
|
||||
changeFeaturePhoto.push(face.person.id);
|
||||
}
|
||||
|
||||
await this.repository.reassignFace(face.id, personId);
|
||||
}
|
||||
|
||||
result.push(person);
|
||||
}
|
||||
if (changeFeaturePhoto.length > 0) {
|
||||
// Remove duplicates
|
||||
await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
|
||||
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id);
|
||||
const face = await this.repository.getFaceById(dto.id);
|
||||
const person = await this.findOrFail(personId);
|
||||
|
||||
await this.repository.reassignFace(face.id, personId);
|
||||
if (person.faceAssetId === null) {
|
||||
await this.createNewFeaturePhoto([person.id]);
|
||||
}
|
||||
if (face.person && face.person.faceAssetId === face.id) {
|
||||
await this.createNewFeaturePhoto([face.person.id]);
|
||||
}
|
||||
|
||||
return await this.findOrFail(personId).then(mapPerson);
|
||||
}
|
||||
|
||||
async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id);
|
||||
const faces = await this.repository.getFaces(dto.id);
|
||||
return faces.map((asset) => mapFaces(asset, authUser));
|
||||
}
|
||||
|
||||
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
|
||||
this.logger.debug(
|
||||
`Changing feature photos for ${changeFeaturePhoto.length} ${changeFeaturePhoto.length > 1 ? 'people' : 'person'}`,
|
||||
);
|
||||
for (const personId of changeFeaturePhoto) {
|
||||
const assetFace = await this.repository.getRandomFace(personId);
|
||||
|
||||
if (assetFace !== null) {
|
||||
await this.repository.update({
|
||||
id: personId,
|
||||
faceAssetId: assetFace.id,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
|
@ -128,7 +212,7 @@ export class PersonService {
|
|||
throw new BadRequestException('Invalid assetId for feature face');
|
||||
}
|
||||
|
||||
person = await this.repository.update({ id, faceAssetId: assetId });
|
||||
person = await this.repository.update({ id, faceAssetId: face.id });
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||
}
|
||||
|
||||
|
@ -255,9 +339,9 @@ export class PersonService {
|
|||
personId = newPerson.id;
|
||||
}
|
||||
|
||||
const faceId: AssetFaceId = { assetId: asset.id, personId };
|
||||
await this.repository.createFace({
|
||||
...faceId,
|
||||
const face = await this.repository.createFace({
|
||||
assetId: asset.id,
|
||||
personId,
|
||||
embedding,
|
||||
imageHeight: rest.imageHeight,
|
||||
imageWidth: rest.imageWidth,
|
||||
|
@ -266,10 +350,11 @@ export class PersonService {
|
|||
boundingBoxY1: rest.boundingBox.y1,
|
||||
boundingBoxY2: rest.boundingBox.y2,
|
||||
});
|
||||
const faceId: AssetFaceId = { assetId: asset.id, personId };
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId });
|
||||
|
||||
if (newPerson) {
|
||||
await this.repository.update({ id: personId, faceAssetId: asset.id });
|
||||
await this.repository.update({ id: personId, faceAssetId: face.id });
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
||||
}
|
||||
}
|
||||
|
@ -304,14 +389,13 @@ export class PersonService {
|
|||
return false;
|
||||
}
|
||||
|
||||
const [face] = await this.repository.getFacesByIds([{ personId: person.id, assetId: person.faceAssetId }]);
|
||||
if (!face) {
|
||||
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
|
||||
if (face === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
assetId,
|
||||
personId,
|
||||
boundingBoxX1: x1,
|
||||
boundingBoxX2: x2,
|
||||
boundingBoxY1: y1,
|
||||
|
@ -324,8 +408,7 @@ export class PersonService {
|
|||
if (!asset?.resizePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Cropping face for person: ${personId}`);
|
||||
this.logger.verbose(`Cropping face for person: ${person.id}`);
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||
this.storageCore.ensureFolders(thumbnailPath);
|
||||
|
||||
|
@ -395,10 +478,6 @@ export class PersonService {
|
|||
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
||||
const assetIds = await this.repository.prepareReassignFaces(mergeData);
|
||||
for (const assetId of assetIds) {
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId: mergeId } });
|
||||
}
|
||||
await this.repository.reassignFaces(mergeData);
|
||||
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } });
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface IAccessRepository {
|
|||
};
|
||||
|
||||
person: {
|
||||
hasFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
|
||||
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ export enum CommunicationEvent {
|
|||
ASSET_DELETE = 'on_asset_delete',
|
||||
ASSET_TRASH = 'on_asset_trash',
|
||||
ASSET_UPDATE = 'on_asset_update',
|
||||
ASSET_HIDDEN = 'on_asset_hidden',
|
||||
ASSET_RESTORE = 'on_asset_restore',
|
||||
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
||||
SERVER_VERSION = 'on_server_version',
|
||||
|
|
|
@ -26,6 +26,10 @@ export interface QueueStatus {
|
|||
isPaused: boolean;
|
||||
}
|
||||
|
||||
export enum QueueCleanType {
|
||||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
export type JobItem =
|
||||
// Transcoding
|
||||
| { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
|
||||
|
@ -120,6 +124,7 @@ export interface IJobRepository {
|
|||
pause(name: QueueName): Promise<void>;
|
||||
resume(name: QueueName): Promise<void>;
|
||||
empty(name: QueueName): Promise<void>;
|
||||
clear(name: QueueName, type: QueueCleanType): Promise<string[]>;
|
||||
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
||||
getJobCounts(name: QueueName): Promise<JobCounts>;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export interface IPersonRepository {
|
|||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
|
||||
getAssets(personId: string): Promise<AssetEntity[]>;
|
||||
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
|
||||
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
|
||||
create(entity: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
|
@ -48,4 +48,8 @@ export interface IPersonRepository {
|
|||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
|
||||
createFace(entity: Partial<AssetFaceEntity>): Promise<AssetFaceEntity>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||
getFaceByIdWithAssets(id: string): Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
|
|
@ -336,14 +336,18 @@ export class AssetService {
|
|||
|
||||
res.set('Cache-Control', 'private, max-age=86400, no-transform');
|
||||
res.header('Content-Type', mimeTypes.lookup(filepath));
|
||||
return new Promise((resolve, reject) => {
|
||||
res.sendFile(filepath, options, (error: Error) => {
|
||||
if (!error) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.message !== 'Request aborted') {
|
||||
this.logger.error(`Unable to send file: ${error.name}`, error.stack);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
AssetsController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
OAuthController,
|
||||
|
@ -50,6 +51,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
|
|||
APIKeyController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
OAuthController,
|
||||
|
|
28
server/src/immich/controllers/face.controller.ts
Normal file
28
server/src/immich/controllers/face.controller.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { AssetFaceResponseDto, AuthUserDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain';
|
||||
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthUser, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@ApiTags('Face')
|
||||
@Controller('face')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class FaceController {
|
||||
constructor(private service: PersonService) {}
|
||||
|
||||
@Get()
|
||||
getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
return this.service.getFacesById(authUser, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
reassignFacesById(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: FaceDto,
|
||||
): Promise<PersonResponseDto> {
|
||||
return this.service.reassignFacesById(authUser, id, dto);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ export * from './app.controller';
|
|||
export * from './asset.controller';
|
||||
export * from './audit.controller';
|
||||
export * from './auth.controller';
|
||||
export * from './face.controller';
|
||||
export * from './job.controller';
|
||||
export * from './library.controller';
|
||||
export * from './oauth.controller';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AssetFaceUpdateDto,
|
||||
AssetResponseDto,
|
||||
AuthUserDto,
|
||||
BulkIdResponseDto,
|
||||
|
@ -34,6 +35,20 @@ export class PersonController {
|
|||
return this.service.getAll(authUser, withHidden);
|
||||
}
|
||||
|
||||
@Post()
|
||||
createPerson(@AuthUser() authUser: AuthUserDto): Promise<PersonResponseDto> {
|
||||
return this.service.createPerson(authUser);
|
||||
}
|
||||
|
||||
@Put(':id/reassign')
|
||||
reassignFaces(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: AssetFaceUpdateDto,
|
||||
): Promise<PersonResponseDto[]> {
|
||||
return this.service.reassignFaces(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Put()
|
||||
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.updatePeople(authUser, dto);
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AssetFaceEntity } from './asset-face.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
@Entity('person')
|
||||
|
@ -40,8 +39,8 @@ export class PersonEntity {
|
|||
@Column({ nullable: true })
|
||||
faceAssetId!: string | null;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAsset!: AssetEntity | null;
|
||||
@ManyToOne(() => AssetFaceEntity, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAsset!: AssetFaceEntity | null;
|
||||
|
||||
@OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person)
|
||||
faces!: AssetFaceEntity[];
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class EditFaceAssetForeignKey1699727044012 implements MigrationInterface {
|
||||
name = 'EditFaceAssetForeignKey1699727044012'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`);
|
||||
await queryRunner.query(`UPDATE person SET "faceAssetId" = asset_faces."id" FROM asset_faces WHERE person."faceAssetId" = asset_faces."assetId" AND person."id" = asset_faces."personId"`)
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`);
|
||||
await queryRunner.query(`UPDATE person SET "faceAssetId" = assets."id" FROM assets, asset_faces WHERE person."faceAssetId" = asset_faces."id" AND asset_faces."assetId" = assets."id"`);
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,6 +5,7 @@ import {
|
|||
ActivityEntity,
|
||||
AlbumEntity,
|
||||
AssetEntity,
|
||||
AssetFaceEntity,
|
||||
LibraryEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
|
@ -20,6 +21,7 @@ export class AccessRepository implements IAccessRepository {
|
|||
@InjectRepository(LibraryEntity) private libraryRepository: Repository<LibraryEntity>,
|
||||
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
|
||||
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
@InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
|
||||
@InjectRepository(UserTokenEntity) private tokenRepository: Repository<UserTokenEntity>,
|
||||
) {}
|
||||
|
@ -318,6 +320,22 @@ export class AccessRepository implements IAccessRepository {
|
|||
})
|
||||
.then((persons) => new Set(persons.map((person) => person.id)));
|
||||
},
|
||||
hasFaceOwnerAccess: async (userId: string, assetFaceIds: Set<string>): Promise<Set<string>> => {
|
||||
if (assetFaceIds.size === 0) {
|
||||
return new Set();
|
||||
}
|
||||
return this.assetFaceRepository
|
||||
.find({
|
||||
select: { id: true },
|
||||
where: {
|
||||
id: In([...assetFaceIds]),
|
||||
asset: {
|
||||
ownerId: userId,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((faces) => new Set(faces.map((face) => face.id)));
|
||||
},
|
||||
};
|
||||
|
||||
partner = {
|
||||
|
|
|
@ -76,7 +76,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
.select('album.id')
|
||||
.addSelect('MIN(assets.fileCreatedAt)', 'start_date')
|
||||
.addSelect('MAX(assets.fileCreatedAt)', 'end_date')
|
||||
.addSelect('COUNT(album_assets.assetsId)', 'asset_count')
|
||||
.addSelect('COUNT(assets.id)', 'asset_count')
|
||||
.leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id')
|
||||
.leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId')
|
||||
.where('album.id IN (:...ids)', { ids })
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue