Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums

This commit is contained in:
Jonathan Jogenfors 2023-10-11 10:46:57 +02:00
commit 4439bc2355
286 changed files with 23395 additions and 15933 deletions

View file

@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
cache: true
- name: Create the Keystore

View file

@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
- name: Install dependencies
run: dart pub get

View file

@ -13,20 +13,15 @@ jobs:
e2e-tests:
name: Run end-to-end test suites
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
with:
submodules: "recursive"
- name: Run e2e tests
run: npm run test:e2e
if: ${{ !cancelled() }}
run: docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
doc-tests:
name: Run documentation checks
@ -149,7 +144,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.3"
flutter-version: "3.13.6"
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
@ -223,15 +218,27 @@ jobs:
--health-retries 5
ports:
- 5432:5432
defaults:
run:
working-directory: ./server
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix server ci
run: npm ci
- name: Build the
run: npm run build
- name: Run existing migrations
run: npm --prefix server run typeorm:migrations:run
run: npm run typeorm:migrations:run
- name: Generate new migrations
continue-on-error: true
run: npm --prefix server run typeorm:migrations:generate ./src/infra/migrations/TestMigration
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-files

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "mobile/.isar"]
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "server/test/assets"]
path = server/test/assets
url = https://github.com/immich-app/test-assets

View file

@ -20,7 +20,7 @@ pull-stage:
docker-compose -f ./docker/docker-compose.staging.yml pull
test-e2e:
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
prod:
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans

6808
cli/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -356,6 +356,25 @@ export interface AllJobStatusResponseDto {
*/
'videoConversion': JobStatusDto;
}
/**
*
* @export
* @interface AssetBulkDeleteDto
*/
export interface AssetBulkDeleteDto {
/**
*
* @type {boolean}
* @memberof AssetBulkDeleteDto
*/
'force'?: boolean;
/**
*
* @type {Array<string>}
* @memberof AssetBulkDeleteDto
*/
'ids': Array<string>;
}
/**
*
* @export
@ -657,6 +676,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'isReadOnly': boolean;
/**
*
* @type {boolean}
* @memberof AssetResponseDto
*/
'isTrashed': boolean;
/**
*
* @type {string}
@ -669,6 +694,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'livePhotoVideoId'?: string | null;
/**
*
* @type {string}
* @memberof AssetResponseDto
*/
'localDateTime': string;
/**
*
* @type {string}
@ -1351,54 +1382,6 @@ export interface CuratedObjectsResponseDto {
*/
'resizePath': string;
}
/**
*
* @export
* @interface DeleteAssetDto
*/
export interface DeleteAssetDto {
/**
*
* @type {Array<string>}
* @memberof DeleteAssetDto
*/
'ids': Array<string>;
}
/**
*
* @export
* @interface DeleteAssetResponseDto
*/
export interface DeleteAssetResponseDto {
/**
*
* @type {string}
* @memberof DeleteAssetResponseDto
*/
'id': string;
/**
*
* @type {DeleteAssetStatus}
* @memberof DeleteAssetResponseDto
*/
'status': DeleteAssetStatus;
}
/**
*
* @export
* @enum {string}
*/
export const DeleteAssetStatus = {
Success: 'SUCCESS',
Failed: 'FAILED'
} as const;
export type DeleteAssetStatus = typeof DeleteAssetStatus[keyof typeof DeleteAssetStatus];
/**
*
* @export
@ -2599,6 +2582,12 @@ export interface SearchResponseDto {
* @interface ServerConfigDto
*/
export interface ServerConfigDto {
/**
*
* @type {boolean}
* @memberof ServerConfigDto
*/
'isInitialized': boolean;
/**
*
* @type {string}
@ -2617,6 +2606,12 @@ export interface ServerConfigDto {
* @memberof ServerConfigDto
*/
'oauthButtonText': string;
/**
*
* @type {number}
* @memberof ServerConfigDto
*/
'trashDays': number;
}
/**
*
@ -2690,6 +2685,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'tagImage': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'trash': boolean;
}
/**
*
@ -3133,6 +3134,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'thumbnail': SystemConfigThumbnailDto;
/**
*
* @type {SystemConfigTrashDto}
* @memberof SystemConfigDto
*/
'trash': SystemConfigTrashDto;
}
/**
*
@ -3588,6 +3595,25 @@ export interface SystemConfigThumbnailDto {
}
/**
*
* @export
* @interface SystemConfigTrashDto
*/
export interface SystemConfigTrashDto {
/**
*
* @type {number}
* @memberof SystemConfigTrashDto
*/
'days': number;
/**
*
* @type {boolean}
* @memberof SystemConfigTrashDto
*/
'enabled': boolean;
}
/**
*
* @export
@ -5676,13 +5702,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {DeleteAssetDto} deleteAssetDto
* @param {AssetBulkDeleteDto} assetBulkDeleteDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteAsset: async (deleteAssetDto: DeleteAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'deleteAssetDto' is not null or undefined
assertParamExists('deleteAsset', 'deleteAssetDto', deleteAssetDto)
deleteAssets: async (assetBulkDeleteDto: AssetBulkDeleteDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetBulkDeleteDto' is not null or undefined
assertParamExists('deleteAssets', 'assetBulkDeleteDto', assetBulkDeleteDto)
const localVarPath = `/asset`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -5711,7 +5737,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(deleteAssetDto, localVarRequestOptions, configuration)
localVarRequestOptions.data = serializeDataIfNeeded(assetBulkDeleteDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
@ -5805,6 +5831,44 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
emptyTrash: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/trash/empty`;
// 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};
@ -5973,10 +6037,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/statistics`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6006,6 +6071,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isFavorite'] = isFavorite;
}
if (isTrashed !== undefined) {
localVarQueryParameter['isTrashed'] = isTrashed;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -6078,11 +6147,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {string} [personId]
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getByTimeBucket', 'size', size)
// verify required parameter 'timeBucket' is not null or undefined
@ -6132,6 +6202,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isFavorite'] = isFavorite;
}
if (isTrashed !== undefined) {
localVarQueryParameter['isTrashed'] = isTrashed;
}
if (timeBucket !== undefined) {
localVarQueryParameter['timeBucket'] = timeBucket;
}
@ -6340,13 +6414,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
* @param {number} day
* @param {number} month
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'timestamp' is not null or undefined
assertParamExists('getMemoryLane', 'timestamp', timestamp)
getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'day' is not null or undefined
assertParamExists('getMemoryLane', 'day', day)
// verify required parameter 'month' is not null or undefined
assertParamExists('getMemoryLane', 'month', month)
const localVarPath = `/asset/memory-lane`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6368,10 +6445,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (timestamp !== undefined) {
localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ?
(timestamp as any).toISOString() :
timestamp;
if (day !== undefined) {
localVarQueryParameter['day'] = day;
}
if (month !== undefined) {
localVarQueryParameter['month'] = month;
}
@ -6436,11 +6515,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {string} [personId]
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBuckets', 'size', size)
const localVarPath = `/asset/time-buckets`;
@ -6488,6 +6568,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isFavorite'] = isFavorite;
}
if (isTrashed !== undefined) {
localVarQueryParameter['isTrashed'] = isTrashed;
}
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
@ -6589,6 +6673,88 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @param {BulkIdsDto} bulkIdsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreAssets: async (bulkIdsDto: BulkIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'bulkIdsDto' is not null or undefined
assertParamExists('restoreAssets', 'bulkIdsDto', bulkIdsDto)
const localVarPath = `/asset/restore`;
// 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)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreTrash: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/trash/restore`;
// 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 {AssetJobsDto} assetJobsDto
@ -7003,12 +7169,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {DeleteAssetDto} deleteAssetDto
* @param {AssetBulkDeleteDto} assetBulkDeleteDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteAsset(deleteAssetDto: DeleteAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DeleteAssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAsset(deleteAssetDto, options);
async deleteAssets(assetBulkDeleteDto: AssetBulkDeleteDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAssets(assetBulkDeleteDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7033,6 +7199,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(id, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async emptyTrash(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.emptyTrash(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Get all AssetEntity belong to the user
* @param {string} [userId]
@ -7072,11 +7247,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, options);
async getAssetStats(isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, isTrashed, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7100,12 +7276,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {string} [personId]
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options);
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7152,12 +7329,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
* @param {number} day
* @param {number} month
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(day, month, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7178,12 +7356,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {string} [personId]
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options);
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7206,6 +7385,25 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {BulkIdsDto} bulkIdsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async restoreAssets(bulkIdsDto: BulkIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreAssets(bulkIdsDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async restoreTrash(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreTrash(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {AssetJobsDto} assetJobsDto
@ -7324,12 +7522,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
},
/**
*
* @param {AssetApiDeleteAssetRequest} requestParameters Request parameters.
* @param {AssetApiDeleteAssetsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteAsset(requestParameters: AssetApiDeleteAssetRequest, options?: AxiosRequestConfig): AxiosPromise<Array<DeleteAssetResponseDto>> {
return localVarFp.deleteAsset(requestParameters.deleteAssetDto, options).then((request) => request(axios, basePath));
deleteAssets(requestParameters: AssetApiDeleteAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.deleteAssets(requestParameters.assetBulkDeleteDto, options).then((request) => request(axios, basePath));
},
/**
*
@ -7349,6 +7547,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
downloadFile(requestParameters: AssetApiDownloadFileRequest, options?: AxiosRequestConfig): AxiosPromise<File> {
return localVarFp.downloadFile(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
emptyTrash(options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.emptyTrash(options).then((request) => request(axios, basePath));
},
/**
* Get all AssetEntity belong to the user
* @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters.
@ -7382,7 +7588,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<AssetStatsResponseDto> {
return localVarFp.getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(axios, basePath));
return localVarFp.getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, options).then((request) => request(axios, basePath));
},
/**
*
@ -7400,7 +7606,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
@ -7443,7 +7649,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise<Array<MemoryLaneResponseDto>> {
return localVarFp.getMemoryLane(requestParameters.timestamp, options).then((request) => request(axios, basePath));
return localVarFp.getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(axios, basePath));
},
/**
*
@ -7461,7 +7667,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
* Get all asset of a device that are in the database, ID only.
@ -7481,6 +7687,23 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreAssets(requestParameters: AssetApiRestoreAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
restoreTrash(options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.restoreTrash(options).then((request) => request(axios, basePath));
},
/**
*
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
@ -7588,17 +7811,17 @@ export interface AssetApiCheckExistingAssetsRequest {
}
/**
* Request parameters for deleteAsset operation in AssetApi.
* Request parameters for deleteAssets operation in AssetApi.
* @export
* @interface AssetApiDeleteAssetRequest
* @interface AssetApiDeleteAssetsRequest
*/
export interface AssetApiDeleteAssetRequest {
export interface AssetApiDeleteAssetsRequest {
/**
*
* @type {DeleteAssetDto}
* @memberof AssetApiDeleteAsset
* @type {AssetBulkDeleteDto}
* @memberof AssetApiDeleteAssets
*/
readonly deleteAssetDto: DeleteAssetDto
readonly assetBulkDeleteDto: AssetBulkDeleteDto
}
/**
@ -7732,6 +7955,13 @@ export interface AssetApiGetAssetStatsRequest {
* @memberof AssetApiGetAssetStats
*/
readonly isFavorite?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetAssetStats
*/
readonly isTrashed?: boolean
}
/**
@ -7817,6 +8047,13 @@ export interface AssetApiGetByTimeBucketRequest {
*/
readonly isFavorite?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetByTimeBucket
*/
readonly isTrashed?: boolean
/**
*
* @type {string}
@ -7888,11 +8125,18 @@ export interface AssetApiGetMapMarkersRequest {
*/
export interface AssetApiGetMemoryLaneRequest {
/**
* Get pictures for +24 hours from this time going back x years
* @type {string}
*
* @type {number}
* @memberof AssetApiGetMemoryLane
*/
readonly timestamp: string
readonly day: number
/**
*
* @type {number}
* @memberof AssetApiGetMemoryLane
*/
readonly month: number
}
/**
@ -7957,6 +8201,13 @@ export interface AssetApiGetTimeBucketsRequest {
*/
readonly isFavorite?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBuckets
*/
readonly isTrashed?: boolean
/**
*
* @type {string}
@ -7993,6 +8244,20 @@ export interface AssetApiImportFileRequest {
readonly importAssetDto: ImportAssetDto
}
/**
* Request parameters for restoreAssets operation in AssetApi.
* @export
* @interface AssetApiRestoreAssetsRequest
*/
export interface AssetApiRestoreAssetsRequest {
/**
*
* @type {BulkIdsDto}
* @memberof AssetApiRestoreAssets
*/
readonly bulkIdsDto: BulkIdsDto
}
/**
* Request parameters for runAssetJobs operation in AssetApi.
* @export
@ -8252,13 +8517,13 @@ export class AssetApi extends BaseAPI {
/**
*
* @param {AssetApiDeleteAssetRequest} requestParameters Request parameters.
* @param {AssetApiDeleteAssetsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public deleteAsset(requestParameters: AssetApiDeleteAssetRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).deleteAsset(requestParameters.deleteAssetDto, options).then((request) => request(this.axios, this.basePath));
public deleteAssets(requestParameters: AssetApiDeleteAssetsRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).deleteAssets(requestParameters.assetBulkDeleteDto, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -8283,6 +8548,16 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).downloadFile(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public emptyTrash(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).emptyTrash(options).then((request) => request(this.axios, this.basePath));
}
/**
* Get all AssetEntity belong to the user
* @param {AssetApiGetAllAssetsRequest} requestParameters Request parameters.
@ -8323,7 +8598,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -8345,7 +8620,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -8398,7 +8673,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -8420,7 +8695,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -8445,6 +8720,27 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public restoreAssets(requestParameters: AssetApiRestoreAssetsRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).restoreAssets(requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public restoreTrash(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).restoreTrash(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AssetApiRunAssetJobsRequest} requestParameters Request parameters.
@ -11849,6 +12145,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchPerson: async (name: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'name' is not null or undefined
assertParamExists('searchPerson', 'name', name)
const localVarPath = `/search/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: '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 (name !== undefined) {
localVarQueryParameter['name'] = name;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -11902,6 +12243,16 @@ export const SearchApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async searchPerson(name: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchPerson(name, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -11929,6 +12280,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
},
/**
*
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
return localVarFp.searchPerson(requestParameters.name, options).then((request) => request(axios, basePath));
},
};
};
@ -12051,6 +12411,20 @@ export interface SearchApiSearchRequest {
readonly motion?: boolean
}
/**
* Request parameters for searchPerson operation in SearchApi.
* @export
* @interface SearchApiSearchPersonRequest
*/
export interface SearchApiSearchPersonRequest {
/**
*
* @type {string}
* @memberof SearchApiSearchPerson
*/
readonly name: string
}
/**
* SearchApi - object-oriented interface
* @export
@ -12078,6 +12452,17 @@ export class SearchApi extends BaseAPI {
public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SearchApi
*/
public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig) {
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
}
}
@ -14702,6 +15087,15 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
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 (admin !== undefined) {
localVarQueryParameter['admin'] = admin;
}

View file

@ -1,16 +0,0 @@
# Database
DB_HOSTNAME=immich-database-test
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test
# Redis
REDIS_HOSTNAME=immich-redis-test
# Upload File Config
UPLOAD_LOCATION=./upload
# WEB
VITE_SERVER_ENDPOINT=http://localhost:2283/api
TYPESENSE_ENABLED=false

View file

@ -121,7 +121,7 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- ${UPLOAD_LOCATION}/postgres:/data
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432

View file

@ -1,5 +1,7 @@
version: "3.8"
# Compose file for dockerized end-to-end testing of the backend
services:
immich-server-test:
image: immich-server-test
@ -8,39 +10,31 @@ services:
dockerfile: Dockerfile
target: builder
command: npm run test:e2e
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- /usr/src/app/node_modules
env_file:
- .env.test
environment:
- NODE_ENV=development
- TYPESENSE_ENABLED=false
- DB_HOSTNAME=immich-database-test
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=e2e_test
- IMMICH_RUN_ALL_TESTS=true
depends_on:
- immich-redis-test
- immich-database-test
networks:
- immich-test-network
immich-redis-test:
container_name: immich-redis-test
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
networks:
- immich-test-network
immich-database-test:
container_name: immich-database-test
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env.test
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- /var/lib/postgresql/data
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: e2e_test
networks:
- immich-test-network
logging:
driver: none
networks:
immich-test-network:

View file

@ -20,22 +20,9 @@ Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app
The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
### Why does my uploaded photo show up with the wrong date or time in Immich?
When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
If the timezone is incorrect in an uploaded photo, check the `DateTimeOriginal` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
```
environment:
- TZ=Europe/Stockholm # <---- Add this line in the microservices config
```
### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes. Also check the disk space of your reverse proxy, in some cases proxies caches requests to disk before passing them on, and if disk space runs out the request fails.
### Why is Immich slow on low-memory systems like the Raspberry Pi?

View file

@ -19,7 +19,7 @@ Users can deploy a custom reverse proxy that forwards requests to Immich's rever
### Nginx example config
Below is an example config for nginx:
Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`.
```nginx
server {

View file

@ -0,0 +1,17 @@
# Testing
## Server
### Unit tests
Unit are run by calling `npm run test` from the `server` directory.
### End to end tests
The backend has an end-to-end test suite that can be called with `npm run test:e2e` from the `server` directory. This will set up a dummy database inside a temporary container and run the tests against it. Setup and teardown is automatically taken care of. That test, however, can not set up all prerequisites to parse file formats, as that is very complex and error-prone. As such, this test excludes some test cases like HEIC file imports. The test suite will also print a friendly warning to remind you that not all tests are being run.
Note that there is a bug in nodejs <20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8.
To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perfom the tests and exit.
If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run test:e2e`.

View file

@ -42,8 +42,26 @@ Finally, files can be deleted from Immich via the `Remove Offline Files` job. An
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. If the import paths are edited in a way that an external file is no longer in any import path, it will be removed from the library in the same way a deleted file would. If the file is moved back to an import path, it will be added again as if it was a new file.
### Troubleshooting
Sometimes, an external library will not scan correctly. This can happen if the immich_server or immich_microservices can't access the files. Here are some things to check:
- Is the external path set correctly?
- In the docker-compose file, are the volumes mounted correctly?
- Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file?
- Are the permissions set correctly?
If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way.
### Security Considerations
:::caution
Please read and understand this section before setting external paths, as there are important security considerations.
:::
For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
@ -59,6 +77,10 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw`
- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg`
### Nightly job
There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
## Usage
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:

View file

@ -66,6 +66,10 @@ ORDER BY
"users"."email";
```
```sql title="Failed file movements"
SELECT * FROM "move_history";
```
## Users
```sql title="List"

9112
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,10 +17,11 @@
"check": "tsc"
},
"dependencies": {
"@docusaurus/core": "^2.4.1",
"@docusaurus/preset-classic": "^2.4.1",
"@docusaurus/core": "^2.4.3",
"@docusaurus/preset-classic": "^2.4.3",
"@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
"clsx": "^1.2.1",
"docusaurus-lunr-search": "^2.3.2",
"docusaurus-preset-openapi": "^0.6.3",

View file

@ -1,7 +1,9 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
"baseUrl": ".",
"module": "Node16"
}
}

View file

@ -16,13 +16,6 @@ from ..config import log
from ..schemas import ModelType
from .base import InferenceModel
_ST_TO_JINA_MODEL_NAME = {
"clip-ViT-B-16": "ViT-B-16::openai",
"clip-ViT-B-32": "ViT-B-32::openai",
"clip-ViT-B-32-multilingual-v1": "M-CLIP/XLM-Roberta-Large-Vit-B-32",
"clip-ViT-L-14": "ViT-L-14::openai",
}
class CLIPEncoder(InferenceModel):
_model_type = ModelType.CLIP
@ -36,11 +29,10 @@ class CLIPEncoder(InferenceModel):
) -> None:
if mode is not None and mode not in ("text", "vision"):
raise ValueError(f"Mode must be 'text', 'vision', or omitted; got '{mode}'")
if "vit-b" not in model_name.lower():
raise ValueError(f"Only ViT-B models are currently supported; got '{model_name}'")
if model_name not in _MODELS:
raise ValueError(f"Unknown model name {model_name}.")
self.mode = mode
jina_model_name = self._get_jina_model_name(model_name)
super().__init__(jina_model_name, cache_dir, **model_kwargs)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self) -> None:
models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
@ -104,20 +96,6 @@ class CLIPEncoder(InferenceModel):
return outputs[0][0].tolist()
def _get_jina_model_name(self, model_name: str) -> str:
if model_name in _MODELS:
return model_name
elif model_name in _ST_TO_JINA_MODEL_NAME:
log.warn(
(
f"Sentence-Transformer models like '{model_name}' are not supported."
f"Using '{_ST_TO_JINA_MODEL_NAME[model_name]}' instead as it is the best match for '{model_name}'."
),
)
return _ST_TO_JINA_MODEL_NAME[model_name]
else:
raise ValueError(f"Unknown model name {model_name}.")
def _download_model(self, model_name: str, model_md5: str) -> bool:
# downloading logic is adapted from clip-server's CLIPOnnxModel class
download_model(

View file

@ -163,7 +163,6 @@
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
@ -322,5 +321,17 @@
"map_no_location_permission_title": "Location Permission denied",
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_location_dialog_cancel": "Cancel",
"map_location_dialog_yes": "Yes"
"map_location_dialog_yes": "Yes",
"trash_page_title": "Trash ({})",
"trash_page_info": "Backed up items will be permanently deleted after {} days",
"trash_page_no_assets": "No trashed assets",
"trash_page_delete": "Delete",
"trash_page_delete_all": "Delete All",
"trash_page_restore": "Restore",
"trash_page_restore_all": "Restore All",
"trash_page_select_btn": "Select",
"trash_page_select_assets_btn": "Select assets",
"trash_page_empty_trash_btn": "Empty trash",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich"
}

View file

@ -169,4 +169,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.12.1
COCOAPODS: 1.11.3

View file

@ -17,6 +17,7 @@ final archiveProvider = StreamProvider<RenderList>((ref) async* {
.ownerIdEqualToAnyChecksum(user.isarId)
.filter()
.isArchivedEqualTo(true)
.isTrashedEqualTo(false)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View file

@ -6,6 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
@ -19,11 +20,14 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/cache/original_image_provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
@ -67,6 +71,12 @@ class GalleryViewerPage extends HookConsumerWidget {
final header = {"Authorization": authToken};
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
final isTrashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final navStack = AutoRouter.of(context).stackData;
final isFromTrash = isTrashEnabled &&
navStack.length > 2 &&
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
Asset asset() => currentAsset;
@ -161,25 +171,47 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
void handleDelete(Asset deleteAsset) {
void handleDelete(Asset deleteAsset) async {
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{deleteAsset},
force: force,
);
if (isDeleted) {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
}
return isDeleted;
}
// Asset is trashed
if (isTrashEnabled && !isFromTrash) {
final isDeleted = await onDelete(false);
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && isDeleted && deleteAsset.isRemote) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
}
return;
}
// Asset is permanently removed
showDialog(
context: context,
builder: (BuildContext _) {
return DeleteDialog(
onDelete: () {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
},
);
return DeleteDialog(onDelete: () => onDelete(true));
},
);
}

View file

@ -81,7 +81,9 @@ class BackupControllerPage extends HookConsumerWidget {
context: context,
msg: "Deleting ${assets.length} assets on the server...",
);
await ref.read(assetProvider.notifier).deleteAssets(assets);
await ref
.read(assetProvider.notifier)
.deleteAssets(assets, force: true);
ImmichToast.show(
context: context,
msg: "Deleted ${assets.length} assets on the server. "

View file

@ -17,6 +17,7 @@ final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
.ownerIdEqualToAnyChecksum(user.isarId)
.filter()
.isFavoriteEqualTo(true)
.isTrashedEqualTo(false)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View file

@ -7,6 +7,7 @@ import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
@ -43,6 +44,8 @@ class ControlBottomAppBar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var hasRemote = selectionAssetState == AssetState.remote;
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
Widget renderActionButtons() {
return Row(
@ -70,14 +73,20 @@ class ControlBottomAppBar extends ConsumerWidget {
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(),
onPressed: enabled
? () => showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
)
? () {
if (!trashEnabled) {
showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
);
} else {
onDelete();
}
}
: null,
),
if (!hasRemote)

View file

@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dar
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
class ProfileDrawer extends HookConsumerWidget {
@ -16,6 +17,9 @@ class ProfileDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
buildSignOutButton() {
return ListTile(
leading: SizedBox(
@ -91,6 +95,29 @@ class ProfileDrawer extends HookConsumerWidget {
);
}
buildTrashButton() {
return ListTile(
leading: SizedBox(
height: double.infinity,
child: Icon(
Icons.delete_rounded,
color: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"Trash",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
).tr(),
onTap: () {
AutoRouter.of(context).push(const TrashRoute());
},
);
}
return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
@ -105,6 +132,7 @@ class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawerHeader(),
buildSettingButton(),
buildAppLogButton(),
if (trashEnabled) buildTrashButton(),
buildSignOutButton(),
],
),

View file

@ -43,6 +43,8 @@ class HomePage extends HookConsumerWidget {
final sharedAlbums = ref.watch(sharedAlbumProvider);
final albumService = ref.watch(albumServiceProvider);
final currentUser = ref.watch(currentUserProvider);
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
@ -139,30 +141,34 @@ class HomePage extends HookConsumerWidget {
void onDelete() async {
processing.value = true;
try {
await ref.read(assetProvider.notifier).deleteAssets(selection.value);
await ref
.read(assetProvider.notifier)
.deleteAssets(selection.value, force: !trashEnabled);
final hasRemote = selection.value.any((a) => a.isRemote);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
final trashOrRemoved =
!trashEnabled ? 'deleted permanently' : 'trashed';
if (hasRemote) {
ImmichToast.show(
context: context,
msg: '${selection.value.length} $assetOrAssets $trashOrRemoved',
gravity: ToastGravity.BOTTOM,
);
}
selectionEnabledHook.value = false;
} finally {
processing.value = false;
}
}
void onUpload() async {
void onUpload() {
processing.value = true;
selectionEnabledHook.value = false;
try {
final Set<Asset> assets = selection.value;
if (assets.length > 30) {
ImmichToast.show(
context: context,
msg: 'home_page_upload_err_limit'.tr(),
gravity: ToastGravity.BOTTOM,
);
} else {
processing.value = false;
selectionEnabledHook.value = false;
await ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, assets);
}
ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, selection.value);
} finally {
processing.value = false;
}

View file

@ -22,9 +22,9 @@ class MemoryService {
Future<List<Memory>?> getMemoryLane() async {
try {
final now = DateTime.now();
final beginningOfDate = DateTime(now.year, now.month, now.day);
final data = await _apiService.assetApi.getMemoryLane(
beginningOfDate,
now.day,
now.month,
);
if (data == null) {

View file

@ -0,0 +1,144 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/trash/services/trash.service.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/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
class TrashNotifier extends StateNotifier<bool> {
final Isar _db;
final Ref _ref;
final TrashService _trashService;
final _log = Logger('TrashNotifier');
TrashNotifier(
this._trashService,
this._db,
this._ref,
) : super(false);
Future<void> emptyTrash() async {
try {
final user = _ref.read(currentUserProvider);
if (user == null) {
return;
}
await _trashService.emptyTrash();
final dbIds = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.idProperty()
.findAll();
await _db.writeTxn(() async {
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
// Refresh assets in background
Future.delayed(
const Duration(seconds: 4),
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
);
} catch (error, stack) {
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
}
}
Future<bool> restoreAssets(Iterable<Asset> assetList) async {
try {
final result = await _trashService.restoreAssets(assetList);
if (result) {
final remoteAssets = assetList.where((a) => a.isRemote).toList();
final updatedAssets = remoteAssets.map((e) {
e.isTrashed = false;
return e;
}).toList();
await _db.writeTxn(() async {
await _db.assets.putAll(updatedAssets);
});
// Refresh assets in background
Future.delayed(
const Duration(seconds: 4),
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
);
return true;
}
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
return false;
}
Future<void> restoreTrash() async {
try {
final user = _ref.read(currentUserProvider);
if (user == null) {
return;
}
await _trashService.restoreTrash();
final assets = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.findAll();
final updatedAssets = assets.map((e) {
e.isTrashed = false;
return e;
}).toList();
await _db.writeTxn(() async {
await _db.assets.putAll(updatedAssets);
});
// Refresh assets in background
Future.delayed(
const Duration(seconds: 4),
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
);
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
}
}
final trashProvider = StateNotifierProvider<TrashNotifier, bool>((ref) {
return TrashNotifier(
ref.watch(trashServiceProvider),
ref.watch(dbProvider),
ref,
);
});
final trashedAssetsProvider = StreamProvider<RenderList>((ref) async* {
final user = ref.read(currentUserProvider);
if (user == null) return;
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(user.isarId)
.isTrashedEqualTo(true)
.sortByFileCreatedAt();
const groupBy = GroupAssetsBy.none;
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
});

View file

@ -0,0 +1,48 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final trashServiceProvider = Provider<TrashService>((ref) {
return TrashService(
ref.watch(apiServiceProvider),
);
});
class TrashService {
final _log = Logger("TrashService");
final ApiService _apiService;
TrashService(this._apiService);
Future<bool> restoreAssets(Iterable<Asset> assetList) async {
try {
List<String> remoteIds =
assetList.where((a) => a.isRemote).map((e) => e.remoteId!).toList();
await _apiService.assetApi.restoreAssets(BulkIdsDto(ids: remoteIds));
return true;
} catch (error, stack) {
_log.severe("Cannot restore assets ${error.toString()}", error, stack);
return false;
}
}
Future<void> emptyTrash() async {
try {
await _apiService.assetApi.emptyTrash();
} catch (error, stack) {
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
}
}
Future<void> restoreTrash() async {
try {
await _apiService.assetApi.restoreTrash();
} catch (error, stack) {
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
}
}
}

View file

@ -0,0 +1,276 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/trash/providers/trashed_asset.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class TrashPage extends HookConsumerWidget {
const TrashPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final trashedAssets = ref.watch(trashedAssetsProvider);
final trashDays =
ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
final selectionEnabledHook = useState(false);
final selection = useState(<Asset>{});
final processing = useState(false);
void selectionListener(
bool multiselect,
Set<Asset> selectedAssets,
) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
}
onEmptyTrash() async {
processing.value = true;
await ref.read(trashProvider.notifier).emptyTrash();
processing.value = false;
selectionEnabledHook.value = false;
if (context.mounted) {
ImmichToast.show(
context: context,
msg: 'Emptied trash',
gravity: ToastGravity.BOTTOM,
);
}
}
handleEmptyTrash() async {
await showDialog(
context: context,
builder: (context) => ConfirmDialog(
onOk: () => onEmptyTrash(),
title: "trash_page_empty_trash_btn".tr(),
ok: "trash_page_empty_trash_dialog_ok".tr(),
content: "trash_page_empty_trash_dialog_content".tr(),
),
);
}
Future<void> onPermanentlyDelete() async {
processing.value = true;
try {
if (selection.value.isNotEmpty) {
await ref
.read(assetProvider.notifier)
.deleteAssets(selection.value, force: true);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
if (context.mounted) {
ImmichToast.show(
context: context,
msg:
'${selection.value.length} $assetOrAssets deleted permanently',
gravity: ToastGravity.BOTTOM,
);
}
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
handlePermanentDelete() async {
await showDialog(
context: context,
builder: (context) => DeleteDialog(
onDelete: () => onPermanentlyDelete(),
),
);
}
Future<void> handleRestoreAll() async {
processing.value = true;
await ref.read(trashProvider.notifier).restoreTrash();
processing.value = false;
selectionEnabledHook.value = false;
}
Future<void> handleRestore() async {
processing.value = true;
try {
if (selection.value.isNotEmpty) {
final result = await ref
.read(trashProvider.notifier)
.restoreAssets(selection.value);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
if (result && context.mounted) {
ImmichToast.show(
context: context,
msg:
'${selection.value.length} $assetOrAssets restored successfully',
gravity: ToastGravity.BOTTOM,
);
}
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
String getAppBarTitle(String count) {
if (selectionEnabledHook.value) {
return selection.value.isNotEmpty
? "${selection.value.length}"
: "trash_page_select_assets_btn".tr();
}
return 'trash_page_title'.tr(args: [count]);
}
AppBar buildAppBar(String count) {
return AppBar(
leading: IconButton(
onPressed: !selectionEnabledHook.value
? () => AutoRouter.of(context).pop()
: () {
selectionEnabledHook.value = false;
selection.value = {};
},
icon: !selectionEnabledHook.value
? const Icon(Icons.arrow_back_ios_rounded)
: const Icon(Icons.close_rounded),
),
centerTitle: !selectionEnabledHook.value,
automaticallyImplyLeading: false,
title: Text(getAppBarTitle(count)),
actions: <Widget>[
if (!selectionEnabledHook.value)
PopupMenuButton<void Function()>(
itemBuilder: (context) {
return [
PopupMenuItem(
value: () => selectionEnabledHook.value = true,
child: const Text('trash_page_select_btn').tr(),
),
PopupMenuItem(
value: handleEmptyTrash,
child: const Text('trash_page_empty_trash_btn').tr(),
),
];
},
onSelected: (fn) => fn(),
),
],
);
}
Widget buildBottomBar() {
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Container(
color: Theme.of(context).canvasColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
icon: Icon(
Icons.delete_forever,
color: Colors.red[400],
),
label: Text(
selection.value.isEmpty
? 'trash_page_delete_all'.tr()
: 'trash_page_delete'.tr(),
style: TextStyle(
fontSize: 14,
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
),
onPressed: processing.value
? null
: selection.value.isEmpty
? handleEmptyTrash
: handlePermanentDelete,
),
TextButton.icon(
icon: const Icon(
Icons.history_rounded,
),
label: Text(
selection.value.isEmpty
? 'trash_page_restore_all'.tr()
: 'trash_page_restore'.tr(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
onPressed: processing.value
? null
: selection.value.isEmpty
? handleRestoreAll
: handleRestore,
),
],
),
),
),
),
);
}
return trashedAssets.when(
loading: () => Scaffold(
appBar: buildAppBar("?"),
body: const Center(child: CircularProgressIndicator()),
),
error: (error, stackTrace) => Scaffold(
appBar: buildAppBar("!"),
body: Center(child: Text(error.toString())),
),
data: (data) => Scaffold(
appBar: buildAppBar(data.totalAssets.toString()),
body: data.isEmpty
? Center(
child: Text('trash_page_no_assets'.tr()),
)
: Stack(
children: [
SafeArea(
child: ImmichAssetGrid(
renderList: data,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
showMultiSelectIndicator: false,
topWidget: Padding(
padding: const EdgeInsets.only(
top: 24,
bottom: 24,
left: 12,
right: 12,
),
child: const Text(
"trash_page_info",
).tr(args: ["$trashDays"]),
),
),
),
if (selectionEnabledHook.value) buildBottomBar(),
if (processing.value)
const Center(child: ImmichLoadingIndicator()),
],
),
),
);
}
}

View file

@ -28,6 +28,7 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
import 'package:immich_mobile/modules/login/views/login_page.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
import 'package:immich_mobile/modules/trash/views/trash_page.dart';
import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
import 'package:immich_mobile/modules/search/views/all_people_page.dart';
import 'package:immich_mobile/modules/search/views/all_videos_page.dart';
@ -155,6 +156,7 @@ part 'router.gr.dart';
AutoRoute(page: MemoryPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: MapPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
],
)
class AppRouter extends _$AppRouter {

View file

@ -312,6 +312,12 @@ class _$AppRouter extends RootStackRouter {
),
);
},
TrashRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const TrashPage(),
);
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
@ -624,6 +630,14 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard,
],
),
RouteConfig(
TrashRoute.name,
path: '/trash-page',
guards: [
authGuard,
duplicateGuard,
],
),
];
}
@ -1394,6 +1408,18 @@ class AlbumOptionsRouteArgs {
}
}
/// generated route for
/// [TrashPage]
class TrashRoute extends PageRouteInfo<void> {
const TrashRoute()
: super(
TrashRoute.name,
path: '/trash-page',
);
static const String name = 'TrashRoute';
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View file

@ -30,7 +30,8 @@ class Asset {
exifInfo =
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
isFavorite = remote.isFavorite,
isArchived = remote.isArchived;
isArchived = remote.isArchived,
isTrashed = remote.isTrashed;
Asset.local(AssetEntity local, List<int> hash)
: localId = local.id,
@ -45,6 +46,7 @@ class Asset {
updatedAt = local.modifiedDateTime,
isFavorite = local.isFavorite,
isArchived = false,
isTrashed = false,
fileCreatedAt = local.createDateTime {
if (fileCreatedAt.year == 1970) {
fileCreatedAt = fileModifiedAt;
@ -74,6 +76,7 @@ class Asset {
this.exifInfo,
required this.isFavorite,
required this.isArchived,
required this.isTrashed,
});
@ignore
@ -138,6 +141,8 @@ class Asset {
bool isArchived;
bool isTrashed;
@ignore
ExifInfo? exifInfo;
@ -194,7 +199,8 @@ class Asset {
livePhotoVideoId == other.livePhotoVideoId &&
isFavorite == other.isFavorite &&
isLocal == other.isLocal &&
isArchived == other.isArchived;
isArchived == other.isArchived &&
isTrashed == other.isTrashed;
}
@override
@ -216,7 +222,8 @@ class Asset {
livePhotoVideoId.hashCode ^
isFavorite.hashCode ^
isLocal.hashCode ^
isArchived.hashCode;
isArchived.hashCode ^
isTrashed.hashCode;
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
@ -229,8 +236,9 @@ class Asset {
width == null && a.width != null ||
height == null && a.height != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
!isRemote && a.isRemote && isArchived != a.isArchived;
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed;
}
/// Returns a new [Asset] with values from this and merged & updated with [a]
@ -261,6 +269,7 @@ class Asset {
livePhotoVideoId: livePhotoVideoId,
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
);
}
} else {
@ -275,6 +284,7 @@ class Asset {
// isFavorite + isArchived are not set by device-only assets
isFavorite: a.isFavorite,
isArchived: a.isArchived,
isTrashed: a.isTrashed,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
);
} else {
@ -306,6 +316,7 @@ class Asset {
String? livePhotoVideoId,
bool? isFavorite,
bool? isArchived,
bool? isTrashed,
ExifInfo? exifInfo,
}) =>
Asset(
@ -325,6 +336,7 @@ class Asset {
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
isFavorite: isFavorite ?? this.isFavorite,
isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed,
exifInfo: exifInfo ?? this.exifInfo,
);
@ -378,7 +390,8 @@ class Asset {
"storage": "$storage",
"width": ${width ?? "N/A"},
"height": ${height ?? "N/A"},
"isArchived": $isArchived
"isArchived": $isArchived,
"isTrashed": $isTrashed,
}""";
}
}

View file

@ -57,39 +57,44 @@ const AssetSchema = CollectionSchema(
name: r'isFavorite',
type: IsarType.bool,
),
r'livePhotoVideoId': PropertySchema(
r'isTrashed': PropertySchema(
id: 8,
name: r'isTrashed',
type: IsarType.bool,
),
r'livePhotoVideoId': PropertySchema(
id: 9,
name: r'livePhotoVideoId',
type: IsarType.string,
),
r'localId': PropertySchema(
id: 9,
id: 10,
name: r'localId',
type: IsarType.string,
),
r'ownerId': PropertySchema(
id: 10,
id: 11,
name: r'ownerId',
type: IsarType.long,
),
r'remoteId': PropertySchema(
id: 11,
id: 12,
name: r'remoteId',
type: IsarType.string,
),
r'type': PropertySchema(
id: 12,
id: 13,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 13,
id: 14,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 14,
id: 15,
name: r'width',
type: IsarType.int,
)
@ -196,13 +201,14 @@ void _assetSerialize(
writer.writeInt(offsets[5], object.height);
writer.writeBool(offsets[6], object.isArchived);
writer.writeBool(offsets[7], object.isFavorite);
writer.writeString(offsets[8], object.livePhotoVideoId);
writer.writeString(offsets[9], object.localId);
writer.writeLong(offsets[10], object.ownerId);
writer.writeString(offsets[11], object.remoteId);
writer.writeByte(offsets[12], object.type.index);
writer.writeDateTime(offsets[13], object.updatedAt);
writer.writeInt(offsets[14], object.width);
writer.writeBool(offsets[8], object.isTrashed);
writer.writeString(offsets[9], object.livePhotoVideoId);
writer.writeString(offsets[10], object.localId);
writer.writeLong(offsets[11], object.ownerId);
writer.writeString(offsets[12], object.remoteId);
writer.writeByte(offsets[13], object.type.index);
writer.writeDateTime(offsets[14], object.updatedAt);
writer.writeInt(offsets[15], object.width);
}
Asset _assetDeserialize(
@ -221,14 +227,15 @@ Asset _assetDeserialize(
id: id,
isArchived: reader.readBool(offsets[6]),
isFavorite: reader.readBool(offsets[7]),
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
localId: reader.readStringOrNull(offsets[9]),
ownerId: reader.readLong(offsets[10]),
remoteId: reader.readStringOrNull(offsets[11]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
isTrashed: reader.readBool(offsets[8]),
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
localId: reader.readStringOrNull(offsets[10]),
ownerId: reader.readLong(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[13]),
width: reader.readIntOrNull(offsets[14]),
updatedAt: reader.readDateTime(offsets[14]),
width: reader.readIntOrNull(offsets[15]),
);
return object;
}
@ -257,19 +264,21 @@ P _assetDeserializeProp<P>(
case 7:
return (reader.readBool(offset)) as P;
case 8:
return (reader.readStringOrNull(offset)) as P;
return (reader.readBool(offset)) as P;
case 9:
return (reader.readStringOrNull(offset)) as P;
case 10:
return (reader.readLong(offset)) as P;
case 11:
return (reader.readStringOrNull(offset)) as P;
case 11:
return (reader.readLong(offset)) as P;
case 12:
return (reader.readStringOrNull(offset)) as P;
case 13:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 13:
return (reader.readDateTime(offset)) as P;
case 14:
return (reader.readDateTime(offset)) as P;
case 15:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -1290,6 +1299,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isTrashed',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> livePhotoVideoIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -2058,6 +2077,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByLivePhotoVideoId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
@ -2252,6 +2283,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByLivePhotoVideoId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
@ -2388,6 +2431,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isTrashed');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByLivePhotoVideoId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -2490,6 +2539,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
});
}
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isTrashed');
});
}
QueryBuilder<Asset, String?, QQueryOperations> livePhotoVideoIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'livePhotoVideoId');

View file

@ -15,7 +15,6 @@ import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart';
class AssetNotifier extends StateNotifier<bool> {
@ -92,23 +91,45 @@ class AssetNotifier extends StateNotifier<bool> {
await _syncService.syncNewAssetToDb(newAsset);
}
Future<void> deleteAssets(Iterable<Asset> deleteAssets) async {
Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, {
bool? force = false,
}) async {
_deleteInProgress = true;
state = true;
try {
final localDeleted = await _deleteLocalAssets(deleteAssets);
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force);
if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
final dbIds = deleteAssets.map((e) => e.id).toList();
List<Asset>? assetsToUpdate;
// Local only assets are permanently deleted for now. So always remove them from db
final dbIds = deleteAssets
.where((a) => a.isLocal && !a.isRemote)
.map((e) => e.id)
.toList();
if (force == null || !force) {
assetsToUpdate = remoteDeleted.map((e) {
e.isTrashed = true;
return e;
}).toList();
} else {
// Add all remote assets to be deleted from isar as since they are permanently deleted
dbIds.addAll(remoteDeleted.map((e) => e.id));
}
await _db.writeTxn(() async {
if (assetsToUpdate != null) {
await _db.assets.putAll(assetsToUpdate);
}
await _db.exifInfos.deleteAll(dbIds);
await _db.assets.deleteAll(dbIds);
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}
Future<List<String>> _deleteLocalAssets(
@ -127,15 +148,14 @@ class AssetNotifier extends StateNotifier<bool> {
return [];
}
Future<Iterable<String>> _deleteRemoteAssets(
Future<Iterable<Asset>> _deleteRemoteAssets(
Iterable<Asset> assetsToDelete,
bool? force,
) async {
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
final List<DeleteAssetResponseDto> deleteAssetResult =
await _assetService.deleteAssets(remote) ?? [];
return deleteAssetResult
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
.map((a) => a.id);
final isSuccess = await _assetService.deleteAssets(remote, force: force);
return isSuccess ? remote : [];
}
Future<void> toggleFavorite(List<Asset> assets, bool status) async {
@ -190,6 +210,7 @@ final assetsProvider =
.ownerIdEqualToAnyChecksum(userId)
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
@ -210,6 +231,7 @@ final remoteAssetsProvider =
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(userId)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =

View file

@ -26,12 +26,15 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
search: true,
sidecar: true,
tagImage: true,
trash: true,
reverseGeocoding: true,
),
serverConfig: ServerConfigDto(
loginPageMessage: "",
mapTileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
oauthButtonText: "",
trashDays: 30,
isInitialized: false,
),
isVersionMismatch: false,
versionMismatchErrorMessage: "",

View file

@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart';
@ -92,6 +93,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
});
socket.on('on_upload_success', _handleOnUploadSuccess);
socket.on('on_config_update', _handleOnConfigUpdate);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
@ -126,6 +128,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
}
}
_handleOnConfigUpdate(dynamic data) {
ref.read(serverInfoProvider.notifier).getServerFeatures();
ref.read(serverInfoProvider.notifier).getServerConfig();
}
}
final websocketProvider =

View file

@ -64,7 +64,9 @@ class AssetService {
Future<List<Asset>?> _getRemoteAssets(User user) async {
try {
final List<AssetResponseDto>? assets =
await _apiService.assetApi.getAllAssets(userId: user.id);
await _apiService.assetApi.getAllAssets(
userId: user.id,
);
if (assets == null) {
return null;
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
@ -84,9 +86,10 @@ class AssetService {
}
}
Future<List<DeleteAssetResponseDto>?> deleteAssets(
Iterable<Asset> deleteAssets,
) async {
Future<bool> deleteAssets(
Iterable<Asset> deleteAssets, {
bool? force = false,
}) async {
try {
final List<String> payload = [];
@ -94,12 +97,17 @@ class AssetService {
payload.add(asset.remoteId!);
}
return await _apiService.assetApi
.deleteAsset(DeleteAssetDto(ids: payload));
await _apiService.assetApi.deleteAssets(
AssetBulkDeleteDto(
ids: payload,
force: force,
),
);
return true;
} catch (error, stack) {
log.severe("Error deleteAssets ${error.toString()}", error, stack);
return null;
}
return false;
}
/// Loads the exif information from the database. If there is none, loads

View file

@ -15,6 +15,7 @@ doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AllJobStatusResponseDto.md
doc/AssetApi.md
doc/AssetBulkDeleteDto.md
doc/AssetBulkUpdateDto.md
doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
@ -53,9 +54,6 @@ doc/CreateTagDto.md
doc/CreateUserDto.md
doc/CuratedLocationsResponseDto.md
doc/CuratedObjectsResponseDto.md
doc/DeleteAssetDto.md
doc/DeleteAssetResponseDto.md
doc/DeleteAssetStatus.md
doc/DownloadArchiveInfo.md
doc/DownloadInfoDto.md
doc/DownloadResponseDto.md
@ -131,6 +129,7 @@ doc/SystemConfigReverseGeocodingDto.md
doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThumbnailDto.md
doc/SystemConfigTrashDto.md
doc/TagApi.md
doc/TagResponseDto.md
doc/TagTypeEnum.md
@ -186,6 +185,7 @@ lib/model/api_key_create_dto.dart
lib/model/api_key_create_response_dto.dart
lib/model/api_key_response_dto.dart
lib/model/api_key_update_dto.dart
lib/model/asset_bulk_delete_dto.dart
lib/model/asset_bulk_update_dto.dart
lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart
@ -222,9 +222,6 @@ lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart
lib/model/curated_locations_response_dto.dart
lib/model/curated_objects_response_dto.dart
lib/model/delete_asset_dto.dart
lib/model/delete_asset_response_dto.dart
lib/model/delete_asset_status.dart
lib/model/download_archive_info.dart
lib/model/download_info_dto.dart
lib/model/download_response_dto.dart
@ -291,6 +288,7 @@ lib/model/system_config_reverse_geocoding_dto.dart
lib/model/system_config_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_thumbnail_dto.dart
lib/model/system_config_trash_dto.dart
lib/model/tag_response_dto.dart
lib/model/tag_type_enum.dart
lib/model/thumbnail_format.dart
@ -322,6 +320,7 @@ test/api_key_create_response_dto_test.dart
test/api_key_response_dto_test.dart
test/api_key_update_dto_test.dart
test/asset_api_test.dart
test/asset_bulk_delete_dto_test.dart
test/asset_bulk_update_dto_test.dart
test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart
@ -360,9 +359,6 @@ test/create_tag_dto_test.dart
test/create_user_dto_test.dart
test/curated_locations_response_dto_test.dart
test/curated_objects_response_dto_test.dart
test/delete_asset_dto_test.dart
test/delete_asset_response_dto_test.dart
test/delete_asset_status_test.dart
test/download_archive_info_test.dart
test/download_info_dto_test.dart
test/download_response_dto_test.dart
@ -438,6 +434,7 @@ test/system_config_reverse_geocoding_dto_test.dart
test/system_config_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart
test/system_config_thumbnail_dto_test.dart
test/system_config_trash_dto_test.dart
test/tag_api_test.dart
test/tag_response_dto_test.dart
test/tag_type_enum_test.dart

View file

@ -90,9 +90,10 @@ Class | Method | HTTP request | Description
*AssetApi* | [**bulkUploadCheck**](doc//AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
*AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset |
*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
*AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
@ -108,6 +109,8 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import |
*AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore |
*AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
@ -151,6 +154,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
@ -201,6 +205,7 @@ Class | Method | HTTP request | Description
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
- [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md)
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
@ -237,9 +242,6 @@ Class | Method | HTTP request | Description
- [CreateUserDto](doc//CreateUserDto.md)
- [CuratedLocationsResponseDto](doc//CuratedLocationsResponseDto.md)
- [CuratedObjectsResponseDto](doc//CuratedObjectsResponseDto.md)
- [DeleteAssetDto](doc//DeleteAssetDto.md)
- [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
- [DeleteAssetStatus](doc//DeleteAssetStatus.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponseDto](doc//DownloadResponseDto.md)
@ -306,6 +308,7 @@ Class | Method | HTTP request | Description
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
- [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
- [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md)
- [SystemConfigTrashDto](doc//SystemConfigTrashDto.md)
- [TagResponseDto](doc//TagResponseDto.md)
- [TagTypeEnum](doc//TagTypeEnum.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)

View file

@ -12,9 +12,10 @@ Method | HTTP request | Description
[**bulkUploadCheck**](AssetApi.md#bulkuploadcheck) | **POST** /asset/bulk-upload-check |
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
[**deleteAssets**](AssetApi.md#deleteassets) | **DELETE** /asset |
[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
[**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
[**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
@ -30,6 +31,8 @@ Method | HTTP request | Description
[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**importFile**](AssetApi.md#importfile) | **POST** /asset/import |
[**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore |
[**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
@ -211,8 +214,8 @@ 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)
# **deleteAsset**
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
# **deleteAssets**
> deleteAssets(assetBulkDeleteDto)
@ -235,13 +238,12 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final deleteAssetDto = DeleteAssetDto(); // DeleteAssetDto |
final assetBulkDeleteDto = AssetBulkDeleteDto(); // AssetBulkDeleteDto |
try {
final result = api_instance.deleteAsset(deleteAssetDto);
print(result);
api_instance.deleteAssets(assetBulkDeleteDto);
} catch (e) {
print('Exception when calling AssetApi->deleteAsset: $e\n');
print('Exception when calling AssetApi->deleteAssets: $e\n');
}
```
@ -249,11 +251,11 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**deleteAssetDto** | [**DeleteAssetDto**](DeleteAssetDto.md)| |
**assetBulkDeleteDto** | [**AssetBulkDeleteDto**](AssetBulkDeleteDto.md)| |
### Return type
[**List<DeleteAssetResponseDto>**](DeleteAssetResponseDto.md)
void (empty response body)
### Authorization
@ -262,7 +264,7 @@ Name | Type | Description | Notes
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
- **Accept**: Not defined
[[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)
@ -380,6 +382,56 @@ 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)
# **emptyTrash**
> emptyTrash()
### 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 = AssetApi();
try {
api_instance.emptyTrash();
} catch (e) {
print('Exception when calling AssetApi->emptyTrash: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[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)
# **getAllAssets**
> List<AssetResponseDto> getAllAssets(userId, isFavorite, isArchived, skip, updatedAfter, ifNoneMatch)
@ -558,7 +610,7 @@ This endpoint does not need any parameter.
[[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)
# **getAssetStats**
> AssetStatsResponseDto getAssetStats(isArchived, isFavorite)
> AssetStatsResponseDto getAssetStats(isArchived, isFavorite, isTrashed)
@ -583,9 +635,10 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
try {
final result = api_instance.getAssetStats(isArchived, isFavorite);
final result = api_instance.getAssetStats(isArchived, isFavorite, isTrashed);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getAssetStats: $e\n');
@ -598,6 +651,7 @@ Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
### Return type
@ -674,7 +728,7 @@ 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)
# **getByTimeBucket**
> List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key)
> List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key)
@ -704,10 +758,11 @@ final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
final key = key_example; // String |
try {
final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key);
final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getByTimeBucket: $e\n');
@ -725,6 +780,7 @@ Name | Type | Description | Notes
**personId** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional]
### Return type
@ -963,7 +1019,7 @@ 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)
# **getMemoryLane**
> List<MemoryLaneResponseDto> getMemoryLane(timestamp)
> List<MemoryLaneResponseDto> getMemoryLane(day, month)
@ -986,10 +1042,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final timestamp = 2013-10-20T19:20:30+01:00; // DateTime | Get pictures for +24 hours from this time going back x years
final day = 56; // int |
final month = 56; // int |
try {
final result = api_instance.getMemoryLane(timestamp);
final result = api_instance.getMemoryLane(day, month);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMemoryLane: $e\n');
@ -1000,7 +1057,8 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**timestamp** | **DateTime**| Get pictures for +24 hours from this time going back x years |
**day** | **int**| |
**month** | **int**| |
### Return type
@ -1073,7 +1131,7 @@ 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)
# **getTimeBuckets**
> List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key)
> List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key)
@ -1102,10 +1160,11 @@ final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isArchived = true; // bool |
final isFavorite = true; // bool |
final isTrashed = true; // bool |
final key = key_example; // String |
try {
final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key);
final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getTimeBuckets: $e\n');
@ -1122,6 +1181,7 @@ Name | Type | Description | Notes
**personId** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional]
### Return type
@ -1251,6 +1311,110 @@ 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)
# **restoreAssets**
> restoreAssets(bulkIdsDto)
### 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 = AssetApi();
final bulkIdsDto = BulkIdsDto(); // BulkIdsDto |
try {
api_instance.restoreAssets(bulkIdsDto);
} catch (e) {
print('Exception when calling AssetApi->restoreAssets: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[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)
# **restoreTrash**
> restoreTrash()
### 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 = AssetApi();
try {
api_instance.restoreTrash();
} catch (e) {
print('Exception when calling AssetApi->restoreTrash: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[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)
# **runAssetJobs**
> runAssetJobs(assetJobsDto)

View file

@ -1,4 +1,4 @@
# openapi.model.DeleteAssetDto
# openapi.model.AssetBulkDeleteDto
## Load the model package
```dart
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**force** | **bool** | | [optional]
**ids** | **List<String>** | | [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)

View file

@ -21,8 +21,10 @@ Name | Type | Description | Notes
**isFavorite** | **bool** | |
**isOffline** | **bool** | |
**isReadOnly** | **bool** | |
**isTrashed** | **bool** | |
**libraryId** | **String** | |
**livePhotoVideoId** | **String** | | [optional]
**localDateTime** | [**DateTime**](DateTime.md) | |
**originalFileName** | **String** | |
**originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]

View file

@ -1,16 +0,0 @@
# openapi.model.DeleteAssetResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
**status** | [**DeleteAssetStatus**](DeleteAssetStatus.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)

View file

@ -11,6 +11,7 @@ Method | HTTP request | Description
------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**search**](SearchApi.md#search) | **GET** /search |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
# **getExploreData**
@ -149,3 +150,58 @@ 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)
# **searchPerson**
> List<PersonResponseDto> searchPerson(name)
### 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 = SearchApi();
final name = name_example; // String |
try {
final result = api_instance.searchPerson(name);
print(result);
} catch (e) {
print('Exception when calling SearchApi->searchPerson: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**name** | **String**| |
### 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**: 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)

View file

@ -8,9 +8,11 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**isInitialized** | **bool** | |
**loginPageMessage** | **String** | |
**mapTileUrl** | **String** | |
**oauthButtonText** | **String** | |
**trashDays** | **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)

View file

@ -19,6 +19,7 @@ Name | Type | Description | Notes
**search** | **bool** | |
**sidecar** | **bool** | |
**tagImage** | **bool** | |
**trash** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -17,6 +17,7 @@ Name | Type | Description | Notes
**reverseGeocoding** | [**SystemConfigReverseGeocodingDto**](SystemConfigReverseGeocodingDto.md) | |
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | |
**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | |
**trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.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)

View file

@ -1,4 +1,4 @@
# openapi.model.DeleteAssetStatus
# openapi.model.SystemConfigTrashDto
## Load the model package
```dart
@ -8,6 +8,8 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**days** | **int** | |
**enabled** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -410,6 +410,20 @@ Name | Type | Description | Notes
### 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 = UserApi();
final admin = true; // bool |
@ -434,7 +448,7 @@ Name | Type | Description | Notes
### Authorization
No authorization required
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers

View file

@ -54,6 +54,7 @@ part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_delete_dto.dart';
part 'model/asset_bulk_update_dto.dart';
part 'model/asset_bulk_upload_check_dto.dart';
part 'model/asset_bulk_upload_check_item.dart';
@ -90,9 +91,6 @@ part 'model/create_tag_dto.dart';
part 'model/create_user_dto.dart';
part 'model/curated_locations_response_dto.dart';
part 'model/curated_objects_response_dto.dart';
part 'model/delete_asset_dto.dart';
part 'model/delete_asset_response_dto.dart';
part 'model/delete_asset_status.dart';
part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart';
@ -159,6 +157,7 @@ part 'model/system_config_reverse_geocoding_dto.dart';
part 'model/system_config_storage_template_dto.dart';
part 'model/system_config_template_storage_option_dto.dart';
part 'model/system_config_thumbnail_dto.dart';
part 'model/system_config_trash_dto.dart';
part 'model/tag_response_dto.dart';
part 'model/tag_type_enum.dart';
part 'model/thumbnail_format.dart';

View file

@ -183,13 +183,13 @@ class AssetApi {
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
/// Parameters:
///
/// * [DeleteAssetDto] deleteAssetDto (required):
Future<Response> deleteAssetWithHttpInfo(DeleteAssetDto deleteAssetDto,) async {
/// * [AssetBulkDeleteDto] assetBulkDeleteDto (required):
Future<Response> deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset';
// ignore: prefer_final_locals
Object? postBody = deleteAssetDto;
Object? postBody = assetBulkDeleteDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@ -211,23 +211,12 @@ class AssetApi {
/// Parameters:
///
/// * [DeleteAssetDto] deleteAssetDto (required):
Future<List<DeleteAssetResponseDto>?> deleteAsset(DeleteAssetDto deleteAssetDto,) async {
final response = await deleteAssetWithHttpInfo(deleteAssetDto,);
/// * [AssetBulkDeleteDto] assetBulkDeleteDto (required):
Future<void> deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto,) async {
final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto,);
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<DeleteAssetResponseDto>') as List)
.cast<DeleteAssetResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'POST /asset/download/archive' operation and returns the [Response].
@ -341,6 +330,39 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'POST /asset/trash/empty' operation and returns the [Response].
Future<Response> emptyTrashWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/trash/empty';
// 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<void> emptyTrash() async {
final response = await emptyTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Get all AssetEntity belong to the user
///
/// Note: This method returns the HTTP [Response].
@ -549,7 +571,9 @@ class AssetApi {
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
Future<Response> getAssetStatsWithHttpInfo({ bool? isArchived, bool? isFavorite, }) async {
///
/// * [bool] isTrashed:
Future<Response> getAssetStatsWithHttpInfo({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/statistics';
@ -566,6 +590,9 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
const contentTypes = <String>[];
@ -586,8 +613,10 @@ class AssetApi {
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
Future<AssetStatsResponseDto?> getAssetStats({ bool? isArchived, bool? isFavorite, }) async {
final response = await getAssetStatsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, );
///
/// * [bool] isTrashed:
Future<AssetStatsResponseDto?> getAssetStats({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
final response = await getAssetStatsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -681,8 +710,10 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-bucket';
@ -708,6 +739,9 @@ class AssetApi {
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
if (key != null) {
@ -744,9 +778,11 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
final response = await getByTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
final response = await getByTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -984,9 +1020,10 @@ class AssetApi {
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
/// Parameters:
///
/// * [DateTime] timestamp (required):
/// Get pictures for +24 hours from this time going back x years
Future<Response> getMemoryLaneWithHttpInfo(DateTime timestamp,) async {
/// * [int] day (required):
///
/// * [int] month (required):
Future<Response> getMemoryLaneWithHttpInfo(int day, int month,) async {
// ignore: prefer_const_declarations
final path = r'/asset/memory-lane';
@ -997,7 +1034,8 @@ class AssetApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'timestamp', timestamp));
queryParams.addAll(_queryParams('', 'day', day));
queryParams.addAll(_queryParams('', 'month', month));
const contentTypes = <String>[];
@ -1015,10 +1053,11 @@ class AssetApi {
/// Parameters:
///
/// * [DateTime] timestamp (required):
/// Get pictures for +24 hours from this time going back x years
Future<List<MemoryLaneResponseDto>?> getMemoryLane(DateTime timestamp,) async {
final response = await getMemoryLaneWithHttpInfo(timestamp,);
/// * [int] day (required):
///
/// * [int] month (required):
Future<List<MemoryLaneResponseDto>?> getMemoryLane(int day, int month,) async {
final response = await getMemoryLaneWithHttpInfo(day, month,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -1104,8 +1143,10 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/time-buckets';
@ -1132,6 +1173,9 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
@ -1164,9 +1208,11 @@ class AssetApi {
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
///
/// * [String] key:
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async {
final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async {
final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -1286,6 +1332,78 @@ class AssetApi {
return null;
}
/// Performs an HTTP 'POST /asset/restore' operation and returns the [Response].
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<Response> restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/restore';
// ignore: prefer_final_locals
Object? postBody = bulkIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [BulkIdsDto] bulkIdsDto (required):
Future<void> restoreAssets(BulkIdsDto bulkIdsDto,) async {
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /asset/trash/restore' operation and returns the [Response].
Future<Response> restoreTrashWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/asset/trash/restore';
// 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<void> restoreTrash() async {
final response = await restoreTrashWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response].
/// Parameters:
///

View file

@ -215,4 +215,56 @@ class SearchApi {
}
return null;
}
/// Performs an HTTP 'GET /search/person' operation and returns the [Response].
/// Parameters:
///
/// * [String] name (required):
Future<Response> searchPersonWithHttpInfo(String name,) async {
// ignore: prefer_const_declarations
final path = r'/search/person';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'name', name));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] name (required):
Future<List<PersonResponseDto>?> searchPerson(String name,) async {
final response = await searchPersonWithHttpInfo(name,);
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;
}
}

View file

@ -199,6 +199,8 @@ class ApiClient {
return AlbumResponseDto.fromJson(value);
case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkDeleteDto':
return AssetBulkDeleteDto.fromJson(value);
case 'AssetBulkUpdateDto':
return AssetBulkUpdateDto.fromJson(value);
case 'AssetBulkUploadCheckDto':
@ -271,12 +273,6 @@ class ApiClient {
return CuratedLocationsResponseDto.fromJson(value);
case 'CuratedObjectsResponseDto':
return CuratedObjectsResponseDto.fromJson(value);
case 'DeleteAssetDto':
return DeleteAssetDto.fromJson(value);
case 'DeleteAssetResponseDto':
return DeleteAssetResponseDto.fromJson(value);
case 'DeleteAssetStatus':
return DeleteAssetStatusTypeTransformer().decode(value);
case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto':
@ -409,6 +405,8 @@ class ApiClient {
return SystemConfigTemplateStorageOptionDto.fromJson(value);
case 'SystemConfigThumbnailDto':
return SystemConfigThumbnailDto.fromJson(value);
case 'SystemConfigTrashDto':
return SystemConfigTrashDto.fromJson(value);
case 'TagResponseDto':
return TagResponseDto.fromJson(value);
case 'TagTypeEnum':

View file

@ -76,9 +76,6 @@ String parameterToString(dynamic value) {
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
if (value is DeleteAssetStatus) {
return DeleteAssetStatusTypeTransformer().encode(value).toString();
}
if (value is EntityType) {
return EntityTypeTypeTransformer().encode(value).toString();
}

View file

@ -10,40 +10,57 @@
part of openapi.api;
class DeleteAssetDto {
/// Returns a new [DeleteAssetDto] instance.
DeleteAssetDto({
class AssetBulkDeleteDto {
/// Returns a new [AssetBulkDeleteDto] instance.
AssetBulkDeleteDto({
this.force,
this.ids = const [],
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? force;
List<String> ids;
@override
bool operator ==(Object other) => identical(this, other) || other is DeleteAssetDto &&
bool operator ==(Object other) => identical(this, other) || other is AssetBulkDeleteDto &&
other.force == force &&
other.ids == ids;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(force == null ? 0 : force!.hashCode) +
(ids.hashCode);
@override
String toString() => 'DeleteAssetDto[ids=$ids]';
String toString() => 'AssetBulkDeleteDto[force=$force, ids=$ids]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.force != null) {
json[r'force'] = this.force;
} else {
// json[r'force'] = null;
}
json[r'ids'] = this.ids;
return json;
}
/// Returns a new [DeleteAssetDto] instance and imports its values from
/// Returns a new [AssetBulkDeleteDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DeleteAssetDto? fromJson(dynamic value) {
static AssetBulkDeleteDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return DeleteAssetDto(
return AssetBulkDeleteDto(
force: mapValueOfType<bool>(json, r'force'),
ids: json[r'ids'] is List
? (json[r'ids'] as List).cast<String>()
: const [],
@ -52,11 +69,11 @@ class DeleteAssetDto {
return null;
}
static List<DeleteAssetDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DeleteAssetDto>[];
static List<AssetBulkDeleteDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkDeleteDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DeleteAssetDto.fromJson(row);
final value = AssetBulkDeleteDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -65,12 +82,12 @@ class DeleteAssetDto {
return result.toList(growable: growable);
}
static Map<String, DeleteAssetDto> mapFromJson(dynamic json) {
final map = <String, DeleteAssetDto>{};
static Map<String, AssetBulkDeleteDto> mapFromJson(dynamic json) {
final map = <String, AssetBulkDeleteDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DeleteAssetDto.fromJson(entry.value);
final value = AssetBulkDeleteDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -79,14 +96,14 @@ class DeleteAssetDto {
return map;
}
// maps a json object with a list of DeleteAssetDto-objects as value to a dart map
static Map<String, List<DeleteAssetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DeleteAssetDto>>{};
// maps a json object with a list of AssetBulkDeleteDto-objects as value to a dart map
static Map<String, List<AssetBulkDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetBulkDeleteDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DeleteAssetDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = AssetBulkDeleteDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

View file

@ -26,8 +26,10 @@ class AssetResponseDto {
required this.isFavorite,
required this.isOffline,
required this.isReadOnly,
required this.isTrashed,
required this.libraryId,
this.livePhotoVideoId,
required this.localDateTime,
required this.originalFileName,
required this.originalPath,
this.owner,
@ -74,10 +76,14 @@ class AssetResponseDto {
bool isReadOnly;
bool isTrashed;
String libraryId;
String? livePhotoVideoId;
DateTime localDateTime;
String originalFileName;
String originalPath;
@ -128,8 +134,10 @@ class AssetResponseDto {
other.isFavorite == isFavorite &&
other.isOffline == isOffline &&
other.isReadOnly == isReadOnly &&
other.isTrashed == isTrashed &&
other.libraryId == libraryId &&
other.livePhotoVideoId == livePhotoVideoId &&
other.localDateTime == localDateTime &&
other.originalFileName == originalFileName &&
other.originalPath == originalPath &&
other.owner == owner &&
@ -158,8 +166,10 @@ class AssetResponseDto {
(isFavorite.hashCode) +
(isOffline.hashCode) +
(isReadOnly.hashCode) +
(isTrashed.hashCode) +
(libraryId.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(localDateTime.hashCode) +
(originalFileName.hashCode) +
(originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) +
@ -173,7 +183,7 @@ class AssetResponseDto {
(updatedAt.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -194,12 +204,14 @@ class AssetResponseDto {
json[r'isFavorite'] = this.isFavorite;
json[r'isOffline'] = this.isOffline;
json[r'isReadOnly'] = this.isReadOnly;
json[r'isTrashed'] = this.isTrashed;
json[r'libraryId'] = this.libraryId;
if (this.livePhotoVideoId != null) {
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
} else {
// json[r'livePhotoVideoId'] = null;
}
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName;
json[r'originalPath'] = this.originalPath;
if (this.owner != null) {
@ -247,8 +259,10 @@ class AssetResponseDto {
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isOffline: mapValueOfType<bool>(json, r'isOffline')!,
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly')!,
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
libraryId: mapValueOfType<String>(json, r'libraryId')!,
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', '')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']),
@ -319,7 +333,9 @@ class AssetResponseDto {
'isFavorite',
'isOffline',
'isReadOnly',
'isTrashed',
'libraryId',
'localDateTime',
'originalFileName',
'originalPath',
'ownerId',

View file

@ -1,85 +0,0 @@
//
// 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 DeleteAssetStatus {
/// Instantiate a new enum with the provided [value].
const DeleteAssetStatus._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const SUCCESS = DeleteAssetStatus._(r'SUCCESS');
static const FAILED = DeleteAssetStatus._(r'FAILED');
/// List of all possible values in this [enum][DeleteAssetStatus].
static const values = <DeleteAssetStatus>[
SUCCESS,
FAILED,
];
static DeleteAssetStatus? fromJson(dynamic value) => DeleteAssetStatusTypeTransformer().decode(value);
static List<DeleteAssetStatus>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <DeleteAssetStatus>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DeleteAssetStatus.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [DeleteAssetStatus] to String,
/// and [decode] dynamic data back to [DeleteAssetStatus].
class DeleteAssetStatusTypeTransformer {
factory DeleteAssetStatusTypeTransformer() => _instance ??= const DeleteAssetStatusTypeTransformer._();
const DeleteAssetStatusTypeTransformer._();
String encode(DeleteAssetStatus data) => data.value;
/// Decodes a [dynamic value][data] to a DeleteAssetStatus.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
DeleteAssetStatus? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'SUCCESS': return DeleteAssetStatus.SUCCESS;
case r'FAILED': return DeleteAssetStatus.FAILED;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [DeleteAssetStatusTypeTransformer] instance.
static DeleteAssetStatusTypeTransformer? _instance;
}

View file

@ -13,38 +13,50 @@ part of openapi.api;
class ServerConfigDto {
/// Returns a new [ServerConfigDto] instance.
ServerConfigDto({
required this.isInitialized,
required this.loginPageMessage,
required this.mapTileUrl,
required this.oauthButtonText,
required this.trashDays,
});
bool isInitialized;
String loginPageMessage;
String mapTileUrl;
String oauthButtonText;
int trashDays;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerConfigDto &&
other.isInitialized == isInitialized &&
other.loginPageMessage == loginPageMessage &&
other.mapTileUrl == mapTileUrl &&
other.oauthButtonText == oauthButtonText;
other.oauthButtonText == oauthButtonText &&
other.trashDays == trashDays;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(isInitialized.hashCode) +
(loginPageMessage.hashCode) +
(mapTileUrl.hashCode) +
(oauthButtonText.hashCode);
(oauthButtonText.hashCode) +
(trashDays.hashCode);
@override
String toString() => 'ServerConfigDto[loginPageMessage=$loginPageMessage, mapTileUrl=$mapTileUrl, oauthButtonText=$oauthButtonText]';
String toString() => 'ServerConfigDto[isInitialized=$isInitialized, loginPageMessage=$loginPageMessage, mapTileUrl=$mapTileUrl, oauthButtonText=$oauthButtonText, trashDays=$trashDays]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'isInitialized'] = this.isInitialized;
json[r'loginPageMessage'] = this.loginPageMessage;
json[r'mapTileUrl'] = this.mapTileUrl;
json[r'oauthButtonText'] = this.oauthButtonText;
json[r'trashDays'] = this.trashDays;
return json;
}
@ -56,9 +68,11 @@ class ServerConfigDto {
final json = value.cast<String, dynamic>();
return ServerConfigDto(
isInitialized: mapValueOfType<bool>(json, r'isInitialized')!,
loginPageMessage: mapValueOfType<String>(json, r'loginPageMessage')!,
mapTileUrl: mapValueOfType<String>(json, r'mapTileUrl')!,
oauthButtonText: mapValueOfType<String>(json, r'oauthButtonText')!,
trashDays: mapValueOfType<int>(json, r'trashDays')!,
);
}
return null;
@ -106,9 +120,11 @@ class ServerConfigDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'isInitialized',
'loginPageMessage',
'mapTileUrl',
'oauthButtonText',
'trashDays',
};
}

View file

@ -24,6 +24,7 @@ class ServerFeaturesDto {
required this.search,
required this.sidecar,
required this.tagImage,
required this.trash,
});
bool clipEncode;
@ -48,6 +49,8 @@ class ServerFeaturesDto {
bool tagImage;
bool trash;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto &&
other.clipEncode == clipEncode &&
@ -60,7 +63,8 @@ class ServerFeaturesDto {
other.reverseGeocoding == reverseGeocoding &&
other.search == search &&
other.sidecar == sidecar &&
other.tagImage == tagImage;
other.tagImage == tagImage &&
other.trash == trash;
@override
int get hashCode =>
@ -75,10 +79,11 @@ class ServerFeaturesDto {
(reverseGeocoding.hashCode) +
(search.hashCode) +
(sidecar.hashCode) +
(tagImage.hashCode);
(tagImage.hashCode) +
(trash.hashCode);
@override
String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, tagImage=$tagImage, trash=$trash]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -93,6 +98,7 @@ class ServerFeaturesDto {
json[r'search'] = this.search;
json[r'sidecar'] = this.sidecar;
json[r'tagImage'] = this.tagImage;
json[r'trash'] = this.trash;
return json;
}
@ -115,6 +121,7 @@ class ServerFeaturesDto {
search: mapValueOfType<bool>(json, r'search')!,
sidecar: mapValueOfType<bool>(json, r'sidecar')!,
tagImage: mapValueOfType<bool>(json, r'tagImage')!,
trash: mapValueOfType<bool>(json, r'trash')!,
);
}
return null;
@ -173,6 +180,7 @@ class ServerFeaturesDto {
'search',
'sidecar',
'tagImage',
'trash',
};
}

View file

@ -22,6 +22,7 @@ class SystemConfigDto {
required this.reverseGeocoding,
required this.storageTemplate,
required this.thumbnail,
required this.trash,
});
SystemConfigFFmpegDto ffmpeg;
@ -42,6 +43,8 @@ class SystemConfigDto {
SystemConfigThumbnailDto thumbnail;
SystemConfigTrashDto trash;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
other.ffmpeg == ffmpeg &&
@ -52,7 +55,8 @@ class SystemConfigDto {
other.passwordLogin == passwordLogin &&
other.reverseGeocoding == reverseGeocoding &&
other.storageTemplate == storageTemplate &&
other.thumbnail == thumbnail;
other.thumbnail == thumbnail &&
other.trash == trash;
@override
int get hashCode =>
@ -65,10 +69,11 @@ class SystemConfigDto {
(passwordLogin.hashCode) +
(reverseGeocoding.hashCode) +
(storageTemplate.hashCode) +
(thumbnail.hashCode);
(thumbnail.hashCode) +
(trash.hashCode);
@override
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, thumbnail=$thumbnail, trash=$trash]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -81,6 +86,7 @@ class SystemConfigDto {
json[r'reverseGeocoding'] = this.reverseGeocoding;
json[r'storageTemplate'] = this.storageTemplate;
json[r'thumbnail'] = this.thumbnail;
json[r'trash'] = this.trash;
return json;
}
@ -101,6 +107,7 @@ class SystemConfigDto {
reverseGeocoding: SystemConfigReverseGeocodingDto.fromJson(json[r'reverseGeocoding'])!,
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!,
trash: SystemConfigTrashDto.fromJson(json[r'trash'])!,
);
}
return null;
@ -157,6 +164,7 @@ class SystemConfigDto {
'reverseGeocoding',
'storageTemplate',
'thumbnail',
'trash',
};
}

View file

@ -10,58 +10,58 @@
part of openapi.api;
class DeleteAssetResponseDto {
/// Returns a new [DeleteAssetResponseDto] instance.
DeleteAssetResponseDto({
required this.id,
required this.status,
class SystemConfigTrashDto {
/// Returns a new [SystemConfigTrashDto] instance.
SystemConfigTrashDto({
required this.days,
required this.enabled,
});
String id;
int days;
DeleteAssetStatus status;
bool enabled;
@override
bool operator ==(Object other) => identical(this, other) || other is DeleteAssetResponseDto &&
other.id == id &&
other.status == status;
bool operator ==(Object other) => identical(this, other) || other is SystemConfigTrashDto &&
other.days == days &&
other.enabled == enabled;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(id.hashCode) +
(status.hashCode);
(days.hashCode) +
(enabled.hashCode);
@override
String toString() => 'DeleteAssetResponseDto[id=$id, status=$status]';
String toString() => 'SystemConfigTrashDto[days=$days, enabled=$enabled]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = this.id;
json[r'status'] = this.status;
json[r'days'] = this.days;
json[r'enabled'] = this.enabled;
return json;
}
/// Returns a new [DeleteAssetResponseDto] instance and imports its values from
/// Returns a new [SystemConfigTrashDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DeleteAssetResponseDto? fromJson(dynamic value) {
static SystemConfigTrashDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return DeleteAssetResponseDto(
id: mapValueOfType<String>(json, r'id')!,
status: DeleteAssetStatus.fromJson(json[r'status'])!,
return SystemConfigTrashDto(
days: mapValueOfType<int>(json, r'days')!,
enabled: mapValueOfType<bool>(json, r'enabled')!,
);
}
return null;
}
static List<DeleteAssetResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DeleteAssetResponseDto>[];
static List<SystemConfigTrashDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigTrashDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DeleteAssetResponseDto.fromJson(row);
final value = SystemConfigTrashDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -70,12 +70,12 @@ class DeleteAssetResponseDto {
return result.toList(growable: growable);
}
static Map<String, DeleteAssetResponseDto> mapFromJson(dynamic json) {
final map = <String, DeleteAssetResponseDto>{};
static Map<String, SystemConfigTrashDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigTrashDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DeleteAssetResponseDto.fromJson(entry.value);
final value = SystemConfigTrashDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -84,14 +84,14 @@ class DeleteAssetResponseDto {
return map;
}
// maps a json object with a list of DeleteAssetResponseDto-objects as value to a dart map
static Map<String, List<DeleteAssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DeleteAssetResponseDto>>{};
// maps a json object with a list of SystemConfigTrashDto-objects as value to a dart map
static Map<String, List<SystemConfigTrashDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigTrashDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DeleteAssetResponseDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = SystemConfigTrashDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
@ -99,8 +99,8 @@ class DeleteAssetResponseDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'id',
'status',
'days',
'enabled',
};
}

View file

@ -38,8 +38,8 @@ void main() {
// TODO
});
//Future<List<DeleteAssetResponseDto>> deleteAsset(DeleteAssetDto deleteAssetDto) async
test('test deleteAsset', () async {
//Future deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto) async
test('test deleteAssets', () async {
// TODO
});
@ -53,6 +53,11 @@ void main() {
// TODO
});
//Future emptyTrash() async
test('test emptyTrash', () async {
// TODO
});
// Get all AssetEntity belong to the user
//
//Future<List<AssetResponseDto>> getAllAssets({ String userId, bool isFavorite, bool isArchived, num skip, DateTime updatedAfter, String ifNoneMatch }) async
@ -72,7 +77,7 @@ void main() {
// TODO
});
//Future<AssetStatsResponseDto> getAssetStats({ bool isArchived, bool isFavorite }) async
//Future<AssetStatsResponseDto> getAssetStats({ bool isArchived, bool isFavorite, bool isTrashed }) async
test('test getAssetStats', () async {
// TODO
});
@ -82,7 +87,7 @@ void main() {
// TODO
});
//Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, String key }) async
//Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, String key }) async
test('test getByTimeBucket', () async {
// TODO
});
@ -107,7 +112,7 @@ void main() {
// TODO
});
//Future<List<MemoryLaneResponseDto>> getMemoryLane(DateTime timestamp) async
//Future<List<MemoryLaneResponseDto>> getMemoryLane(int day, int month) async
test('test getMemoryLane', () async {
// TODO
});
@ -117,7 +122,7 @@ void main() {
// TODO
});
//Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, String key }) async
//Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, String key }) async
test('test getTimeBuckets', () async {
// TODO
});
@ -134,6 +139,16 @@ void main() {
// TODO
});
//Future restoreAssets(BulkIdsDto bulkIdsDto) async
test('test restoreAssets', () async {
// TODO
});
//Future restoreTrash() async
test('test restoreTrash', () async {
// TODO
});
//Future runAssetJobs(AssetJobsDto assetJobsDto) async
test('test runAssetJobs', () async {
// TODO

View file

@ -11,11 +11,16 @@
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for DeleteAssetDto
// tests for AssetBulkDeleteDto
void main() {
// final instance = DeleteAssetDto();
// final instance = AssetBulkDeleteDto();
group('test AssetBulkDeleteDto', () {
// bool force
test('to test the property `force`', () async {
// TODO
});
group('test DeleteAssetDto', () {
// List<String> ids (default value: const [])
test('to test the property `ids`', () async {
// TODO

View file

@ -82,6 +82,11 @@ void main() {
// TODO
});
// bool isTrashed
test('to test the property `isTrashed`', () async {
// TODO
});
// String libraryId
test('to test the property `libraryId`', () async {
// TODO
@ -92,6 +97,11 @@ void main() {
// TODO
});
// DateTime localDateTime
test('to test the property `localDateTime`', () async {
// TODO
});
// String originalFileName
test('to test the property `originalFileName`', () async {
// TODO

View file

@ -1,21 +0,0 @@
//
// 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 DeleteAssetStatus
void main() {
group('test DeleteAssetStatus', () {
});
}

View file

@ -27,5 +27,10 @@ void main() {
// TODO
});
//Future<List<PersonResponseDto>> searchPerson(String name) async
test('test searchPerson', () async {
// TODO
});
});
}

View file

@ -16,6 +16,11 @@ void main() {
// final instance = ServerConfigDto();
group('test ServerConfigDto', () {
// bool isInitialized
test('to test the property `isInitialized`', () async {
// TODO
});
// String loginPageMessage
test('to test the property `loginPageMessage`', () async {
// TODO
@ -31,6 +36,11 @@ void main() {
// TODO
});
// int trashDays
test('to test the property `trashDays`', () async {
// TODO
});
});

View file

@ -71,6 +71,11 @@ void main() {
// TODO
});
// bool trash
test('to test the property `trash`', () async {
// TODO
});
});

View file

@ -61,6 +61,11 @@ void main() {
// TODO
});
// SystemConfigTrashDto trash
test('to test the property `trash`', () async {
// TODO
});
});

View file

@ -11,18 +11,18 @@
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for DeleteAssetResponseDto
// tests for SystemConfigTrashDto
void main() {
// final instance = DeleteAssetResponseDto();
// final instance = SystemConfigTrashDto();
group('test DeleteAssetResponseDto', () {
// String id
test('to test the property `id`', () async {
group('test SystemConfigTrashDto', () {
// int days
test('to test the property `days`', () async {
// TODO
});
// DeleteAssetStatus status
test('to test the property `status`', () async {
// bool enabled
test('to test the property `enabled`', () async {
// TODO
});

View file

@ -1071,10 +1071,10 @@ packages:
dependency: "direct main"
description:
name: photo_manager
sha256: b2d81bd197323697d1b335e2e04cea2f67e11624ced77cfd02917a10afaeba73
sha256: "41eaa1d1fa51bac1c8f2f6debfd34074edcc6b330aa96bb3d33c3bc2fc6c8a5c"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
version: "2.7.2"
platform:
dependency: transitive
description:

View file

@ -13,7 +13,7 @@ dependencies:
sdk: flutter
path_provider_ios:
photo_manager: ^2.5.0
photo_manager: ^2.7.2
flutter_hooks: ^0.18.6
hooks_riverpod: ^2.2.0
cached_network_image: ^3.2.2

View file

@ -24,6 +24,7 @@ void main() {
fileName: '',
isFavorite: false,
isArchived: false,
isTrashed: false,
),
);
}

View file

@ -34,6 +34,7 @@ void main() {
fileName: localId ?? remoteId ?? "",
isFavorite: false,
isArchived: false,
isTrashed: false,
);
}

View file

@ -681,30 +681,20 @@
},
"/asset": {
"delete": {
"operationId": "deleteAsset",
"operationId": "deleteAssets",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteAssetDto"
"$ref": "#/components/schemas/AssetBulkDeleteDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/DeleteAssetResponseDto"
},
"type": "array"
}
}
},
"204": {
"description": ""
}
},
@ -1477,13 +1467,19 @@
"operationId": "getMemoryLane",
"parameters": [
{
"name": "timestamp",
"name": "day",
"required": true,
"in": "query",
"description": "Get pictures for +24 hours from this time going back x years",
"schema": {
"format": "date-time",
"type": "string"
"type": "integer"
}
},
{
"name": "month",
"required": true,
"in": "query",
"schema": {
"type": "integer"
}
}
],
@ -1562,6 +1558,41 @@
]
}
},
"/asset/restore": {
"post": {
"operationId": "restoreAssets",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkIdsDto"
}
}
},
"required": true
},
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Asset"
]
}
},
"/asset/search": {
"post": {
"operationId": "searchAsset",
@ -1661,6 +1692,14 @@
"schema": {
"type": "boolean"
}
},
{
"name": "isTrashed",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
@ -1811,6 +1850,14 @@
"type": "boolean"
}
},
{
"name": "isTrashed",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "timeBucket",
"required": true,
@ -1923,6 +1970,14 @@
"type": "boolean"
}
},
{
"name": "isTrashed",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "key",
"required": false,
@ -1972,6 +2027,56 @@
]
}
},
"/asset/trash/empty": {
"post": {
"operationId": "emptyTrash",
"parameters": [],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Asset"
]
}
},
"/asset/trash/restore": {
"post": {
"operationId": "restoreTrash",
"parameters": [],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Asset"
]
}
},
"/asset/upload": {
"post": {
"operationId": "uploadFile",
@ -3684,6 +3789,50 @@
]
}
},
"/search/person": {
"get": {
"operationId": "searchPerson",
"parameters": [
{
"name": "name",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/PersonResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/server-info": {
"get": {
"operationId": "getServerInfo",
@ -4855,6 +5004,17 @@
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"User"
]
@ -5392,6 +5552,24 @@
],
"type": "object"
},
"AssetBulkDeleteDto": {
"properties": {
"force": {
"type": "boolean"
},
"ids": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
},
"AssetBulkUpdateDto": {
"properties": {
"ids": {
@ -5610,6 +5788,9 @@
"isReadOnly": {
"type": "boolean"
},
"isTrashed": {
"type": "boolean"
},
"libraryId": {
"type": "string"
},
@ -5617,6 +5798,10 @@
"nullable": true,
"type": "string"
},
"localDateTime": {
"format": "date-time",
"type": "string"
},
"originalFileName": {
"type": "string"
},
@ -5676,6 +5861,8 @@
"updatedAt",
"isFavorite",
"isArchived",
"isTrashed",
"localDateTime",
"isOffline",
"isExternal",
"isReadOnly",
@ -6211,48 +6398,6 @@
],
"type": "object"
},
"DeleteAssetDto": {
"properties": {
"ids": {
"example": [
"bf973405-3f2a-48d2-a687-2ed4167164be",
"dd41870b-5d00-46d2-924e-1d8489a0aa0f",
"fad77c3f-deef-4e7e-9608-14c1aa4e559a"
],
"items": {
"type": "string"
},
"title": "Array of asset IDs to delete",
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
},
"DeleteAssetResponseDto": {
"properties": {
"id": {
"type": "string"
},
"status": {
"$ref": "#/components/schemas/DeleteAssetStatus"
}
},
"required": [
"status",
"id"
],
"type": "object"
},
"DeleteAssetStatus": {
"enum": [
"SUCCESS",
"FAILED"
],
"type": "string"
},
"DownloadArchiveInfo": {
"properties": {
"assetIds": {
@ -7206,6 +7351,9 @@
},
"ServerConfigDto": {
"properties": {
"isInitialized": {
"type": "boolean"
},
"loginPageMessage": {
"type": "string"
},
@ -7214,12 +7362,17 @@
},
"oauthButtonText": {
"type": "string"
},
"trashDays": {
"type": "integer"
}
},
"required": [
"trashDays",
"oauthButtonText",
"loginPageMessage",
"mapTileUrl"
"mapTileUrl",
"isInitialized"
],
"type": "object"
},
@ -7257,6 +7410,9 @@
},
"tagImage": {
"type": "boolean"
},
"trash": {
"type": "boolean"
}
},
"required": [
@ -7264,6 +7420,7 @@
"configFile",
"facialRecognition",
"map",
"trash",
"reverseGeocoding",
"oauth",
"oauthAutoLaunch",
@ -7619,6 +7776,9 @@
},
"thumbnail": {
"$ref": "#/components/schemas/SystemConfigThumbnailDto"
},
"trash": {
"$ref": "#/components/schemas/SystemConfigTrashDto"
}
},
"required": [
@ -7630,7 +7790,8 @@
"reverseGeocoding",
"storageTemplate",
"job",
"thumbnail"
"thumbnail",
"trash"
],
"type": "object"
},
@ -7980,6 +8141,21 @@
],
"type": "object"
},
"SystemConfigTrashDto": {
"properties": {
"days": {
"type": "integer"
},
"enabled": {
"type": "boolean"
}
},
"required": [
"days",
"enabled"
],
"type": "object"
},
"TagResponseDto": {
"properties": {
"id": {

7768
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,13 +26,13 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config test/e2e/jest-e2e.json --runInBand",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
"typeorm:migrations:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:create",
"typeorm:migrations:generate": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./src/infra/database.config.ts",
"typeorm:migrations:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d ./src/infra/database.config.ts",
"typeorm:migrations:revert": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:revert -d ./src/infra/database.config.ts",
"typeorm:schema:drop": "node --require ts-node/register ./node_modules/typeorm/cli.js schema:drop -d ./src/infra/database.config.ts",
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand --forceExit",
"typeorm": "typeorm",
"typeorm:migrations:create": "typeorm migration:create",
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",
"typeorm:migrations:run": "typeorm migration:run -d ./dist/infra/database.config.js",
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/infra/database.config.js",
"typeorm:schema:drop": "typeorm schema:drop -d ./dist/infra/database.config.js",
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
"api:typescript": "bash ./bin/generate-open-api.sh web",
"api:dart": "bash ./bin/generate-open-api.sh mobile",
@ -126,7 +126,8 @@
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"utimes": "^5.2.1"
},
"jest": {
"clearMocks": true,

View file

@ -1,12 +1,13 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { IAccessRepository } from './access.repository';
import { IAccessRepository } from '../repositories';
export enum Permission {
// ASSET_CREATE = 'asset.create',
ASSET_READ = 'asset.read',
ASSET_UPDATE = 'asset.update',
ASSET_DELETE = 'asset.delete',
ASSET_RESTORE = 'asset.restore',
ASSET_SHARE = 'asset.share',
ASSET_VIEW = 'asset.view',
ASSET_DOWNLOAD = 'asset.download',
@ -128,6 +129,9 @@ export class AccessCore {
case Permission.ASSET_DELETE:
return this.repository.asset.hasOwnerAccess(authUser.id, id);
case Permission.ASSET_RESTORE:
return this.repository.asset.hasOwnerAccess(authUser.id, id);
case Permission.ASSET_SHARE:
return (
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||

View file

@ -1,2 +1 @@
export * from './access.core';
export * from './access.repository';

View file

@ -12,10 +12,9 @@ import {
userStub,
} from '@test';
import _ from 'lodash';
import { BulkIdErrorReason, IAssetRepository } from '../asset';
import { IJobRepository, JobName } from '../job';
import { IUserRepository } from '../user';
import { IAlbumRepository } from './album.repository';
import { BulkIdErrorReason } from '../asset';
import { JobName } from '../job';
import { IAlbumRepository, IAssetRepository, IJobRepository, IUserRepository } from '../repositories';
import { AlbumService } from './album.service';
describe(AlbumService.name, () => {

View file

@ -1,10 +1,17 @@
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, IAccessRepository, Permission } from '../access';
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto, IAssetRepository } from '../asset';
import { AccessCore, Permission } from '../access';
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset';
import { AuthUserDto } from '../auth';
import { IJobRepository, JobName } from '../job';
import { IUserRepository } from '../user';
import { JobName } from '../job';
import {
AlbumInfoOptions,
IAccessRepository,
IAlbumRepository,
IAssetRepository,
IJobRepository,
IUserRepository,
} from '../repositories';
import {
AlbumCountResponseDto,
AlbumResponseDto,
@ -12,7 +19,6 @@ import {
mapAlbumWithAssets,
mapAlbumWithoutAssets,
} from './album-response.dto';
import { AlbumInfoOptions, IAlbumRepository } from './album.repository';
import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
@Injectable()

View file

@ -1,4 +1,3 @@
export * from './album-response.dto';
export * from './album.repository';
export * from './album.service';
export * from './dto';

View file

@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { authStub, keyStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '@test';
import { ICryptoRepository } from '../crypto';
import { IKeyRepository } from './api-key.repository';
import { ICryptoRepository, IKeyRepository } from '../repositories';
import { APIKeyService } from './api-key.service';
describe(APIKeyService.name, () => {

View file

@ -1,9 +1,8 @@
import { APIKeyEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { ICryptoRepository, IKeyRepository } from '../repositories';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
import { IKeyRepository } from './api-key.repository';
@Injectable()
export class APIKeyService {

View file

@ -1,3 +1,2 @@
export * from './api-key.dto';
export * from './api-key.repository';
export * from './api-key.service';

View file

@ -1,21 +1,36 @@
import { AssetType } from '@app/infra/entities';
import { AssetEntity, AssetType } from '@app/infra/entities';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import {
IAccessRepositoryMock,
assetStub,
authStub,
faceStub,
newAccessRepositoryMock,
newAssetRepositoryMock,
newCommunicationRepositoryMock,
newCryptoRepositoryMock,
newJobRepositoryMock,
newMoveRepositoryMock,
newPersonRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
} from '@test';
import { when } from 'jest-when';
import { Readable } from 'stream';
import { ICryptoRepository } from '../crypto';
import { IJobRepository, JobName } from '../job';
import { IStorageRepository } from '../storage';
import { AssetStats, IAssetRepository, TimeBucketSize } from './asset.repository';
import { JobName } from '../job';
import {
AssetStats,
IAssetRepository,
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IMoveRepository,
IPersonRepository,
IStorageRepository,
ISystemConfigRepository,
JobItem,
TimeBucketSize,
} from '../repositories';
import { AssetService, UploadFieldName } from './asset.service';
import { AssetJobName, AssetStatsResponseDto, DownloadResponseDto } from './dto';
import { mapAsset } from './response-dto';
@ -149,7 +164,11 @@ describe(AssetService.name, () => {
let assetMock: jest.Mocked<IAssetRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let moveMock: jest.Mocked<IMoveRepository>;
let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
it('should work', () => {
expect(sut).toBeDefined();
@ -158,10 +177,31 @@ describe(AssetService.name, () => {
beforeEach(async () => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
communicationMock = newCommunicationRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, storageMock);
configMock = newSystemConfigRepositoryMock();
sut = new AssetService(
accessMock,
assetMock,
cryptoMock,
jobMock,
configMock,
moveMock,
personMock,
storageMock,
communicationMock,
);
when(assetMock.getById)
.calledWith(assetStub.livePhotoStillAsset.id)
.mockResolvedValue(assetStub.livePhotoStillAsset as AssetEntity);
when(assetMock.getById)
.calledWith(assetStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetStub.livePhotoMotionAsset as AssetEntity);
});
describe('canUpload', () => {
@ -274,60 +314,24 @@ describe(AssetService.name, () => {
});
describe('getMemoryLane', () => {
it('should get pictures for each year', async () => {
assetMock.getByDate.mockResolvedValue([]);
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 10 })).resolves.toEqual(
[],
);
expect(assetMock.getByDate).toHaveBeenCalledTimes(10);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2020-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2019-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2018-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2017-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2016-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2015-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2014-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2013-06-15T00:00:00.000Z')],
]);
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15'));
});
it('should keep hours from the date', async () => {
assetMock.getByDate.mockResolvedValue([]);
await expect(
sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15, 5), years: 2 }),
).resolves.toEqual([]);
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T05:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
]);
afterAll(() => {
jest.useRealTimers();
});
it('should set the title correctly', async () => {
when(assetMock.getByDate)
.calledWith(authStub.admin.id, new Date('2022-06-15T00:00:00.000Z'))
.mockResolvedValue([assetStub.image]);
when(assetMock.getByDate)
.calledWith(authStub.admin.id, new Date('2021-06-15T00:00:00.000Z'))
.mockResolvedValue([assetStub.video]);
assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 2 })).resolves.toEqual([
await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
{ title: '1 year since...', assets: [mapAsset(assetStub.image)] },
{ title: '2 years since...', assets: [mapAsset(assetStub.video)] },
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] },
]);
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
]);
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.id, { day: 15, month: 1 }]]);
});
});
@ -512,7 +516,9 @@ describe(AssetService.name, () => {
downloadResponse,
);
expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.id);
expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.id, {
isVisible: true,
});
});
it('should split archives by size', async () => {
@ -632,6 +638,203 @@ describe(AssetService.name, () => {
});
});
describe('deleteAll', () => {
it('should required asset delete access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.deleteAll(authStub.user1, {
ids: ['asset-1'],
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('should force delete a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.ASSET_DELETION, data: { id: 'asset1' } }],
[{ name: JobName.ASSET_DELETION, data: { id: 'asset2' } }],
]);
});
it('should soft delete a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false });
expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['asset1', 'asset2']);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.SEARCH_REMOVE_ASSET,
data: { ids: ['asset1', 'asset2'] },
},
],
]);
});
});
describe('restoreAll', () => {
it('should required asset restore access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.deleteAll(authStub.user1, {
ids: ['asset-1'],
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('should restore a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
await sut.restoreAll(authStub.user1, { ids: ['asset1', 'asset2'] });
expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.SEARCH_INDEX_ASSET,
data: { ids: ['asset1', 'asset2'] },
},
],
]);
});
});
describe('handleAssetDeletion', () => {
beforeEach(() => {
when(jobMock.queue)
.calledWith(
expect.objectContaining({
name: JobName.ASSET_DELETION,
}),
)
.mockImplementation(async (item: JobItem) => {
const jobData = (item as { data?: any })?.data || {};
await sut.handleAssetDeletion(jobData);
});
});
it('should remove faces', async () => {
const assetWithFace = { ...(assetStub.image as AssetEntity), faces: [faceStub.face1, faceStub.mergeFace1] };
when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace);
await sut.handleAssetDeletion({ id: assetWithFace.id });
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.SEARCH_REMOVE_FACE,
data: { assetId: faceStub.face1.assetId, personId: faceStub.face1.personId },
},
],
[
{
name: JobName.SEARCH_REMOVE_FACE,
data: { assetId: faceStub.mergeFace1.assetId, personId: faceStub.mergeFace1.personId },
},
],
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [assetWithFace.id] } }],
[
{
name: JobName.DELETE_FILES,
data: {
files: [
assetWithFace.webpPath,
assetWithFace.resizePath,
assetWithFace.encodedVideoPath,
assetWithFace.sidecarPath,
assetWithFace.originalPath,
],
},
},
],
]);
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
});
it('should not schedule delete-files job for readonly assets', async () => {
when(assetMock.getById)
.calledWith(assetStub.readOnly.id)
.mockResolvedValue(assetStub.readOnly as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.readOnly.id });
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [assetStub.readOnly.id] } }],
]);
expect(assetMock.remove).toHaveBeenCalledWith(assetStub.readOnly);
});
it('should not process assets from external library without fromExternal flag', async () => {
when(assetMock.getById)
.calledWith(assetStub.external.id)
.mockResolvedValue(assetStub.external as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.external.id });
expect(jobMock.queue).not.toBeCalled();
expect(assetMock.remove).not.toBeCalled();
});
it('should process assets from external library with fromExternal flag', async () => {
when(assetMock.getById)
.calledWith(assetStub.external.id)
.mockResolvedValue(assetStub.external as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
expect(assetMock.remove).toHaveBeenCalledWith(assetStub.external);
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [assetStub.external.id] } }],
[
{
name: JobName.DELETE_FILES,
data: {
files: [
assetStub.external.webpPath,
assetStub.external.resizePath,
assetStub.external.encodedVideoPath,
assetStub.external.sidecarPath,
],
},
},
],
]);
});
it('should delete a live photo', async () => {
await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id });
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [assetStub.livePhotoStillAsset.id] } }],
[{ name: JobName.ASSET_DELETION, data: { id: assetStub.livePhotoMotionAsset.id } }],
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [assetStub.livePhotoMotionAsset.id] } }],
[
{
name: JobName.DELETE_FILES,
data: {
files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.mp4'],
},
},
],
[
{
name: JobName.DELETE_FILES,
data: {
files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'],
},
},
],
]);
});
});
describe('run', () => {
it('should run the refresh metadata job', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);

View file

@ -1,17 +1,31 @@
import { AssetEntity } from '@app/infra/entities';
import { AssetEntity, LibraryType } from '@app/infra/entities';
import { BadRequestException, Inject, Logger } from '@nestjs/common';
import { DateTime } from 'luxon';
import _ from 'lodash';
import { DateTime, Duration } from 'luxon';
import { extname } from 'path';
import sanitize from 'sanitize-filename';
import { AccessCore, IAccessRepository, Permission } from '../access';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util';
import { IJobRepository, JobName } from '../job';
import { IStorageRepository, ImmichReadStream, StorageCore, StorageFolder } from '../storage';
import { IAssetRepository } from './asset.repository';
import { IAssetDeletionJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
import {
CommunicationEvent,
IAccessRepository,
IAssetRepository,
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IMoveRepository,
IPersonRepository,
IStorageRepository,
ISystemConfigRepository,
ImmichReadStream,
} from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config';
import {
AssetBulkDeleteDto,
AssetBulkUpdateDto,
AssetIdsDto,
AssetJobName,
@ -24,11 +38,13 @@ import {
MemoryLaneDto,
TimeBucketAssetDto,
TimeBucketDto,
TrashAction,
UpdateAssetDto,
mapStats,
} from './dto';
import {
AssetResponseDto,
BulkIdsDto,
MapMarkerResponseDto,
MemoryLaneResponseDto,
TimeBucketResponseDto,
@ -57,6 +73,7 @@ export interface UploadFile {
export class AssetService {
private logger = new Logger(AssetService.name);
private access: AccessCore;
private configCore: SystemConfigCore;
private storageCore: StorageCore;
constructor(
@ -64,10 +81,15 @@ export class AssetService {
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
) {
this.access = new AccessCore(accessRepository);
this.storageCore = new StorageCore(storageRepository);
this.configCore = SystemConfigCore.create(configRepository);
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
}
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
@ -138,34 +160,36 @@ export class AssetService {
}
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const target = DateTime.fromJSDate(dto.timestamp);
const currentYear = new Date().getFullYear();
const assets = await this.assetRepository.getByDayOfYear(authUser.id, dto);
const onRequest = async (yearsAgo: number): Promise<MemoryLaneResponseDto> => {
const assets = await this.assetRepository.getByDate(authUser.id, target.minus({ years: yearsAgo }).toJSDate());
return {
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
assets: assets.map((a) => mapAsset(a)),
};
};
return _.chain(assets)
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
.map((asset) => {
const years = currentYear - asset.localDateTime.getFullYear();
const requests: Promise<MemoryLaneResponseDto>[] = [];
for (let i = 1; i <= dto.years; i++) {
requests.push(onRequest(i));
}
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
return {
title: `${years} year${years > 1 ? 's' : ''} since...`,
asset: mapAsset(asset),
};
})
.groupBy((asset) => asset.title)
.map((items, title) => ({ title, assets: items.map(({ asset }) => asset) }))
.value();
}
private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) {
if (dto.albumId) {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]);
} else if (dto.userId) {
} else {
dto.userId = dto.userId || authUser.id;
}
if (dto.userId) {
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, [dto.userId]);
if (dto.isArchived !== false) {
await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]);
}
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, [dto.userId]);
} else {
dto.userId = authUser.id;
}
}
@ -274,7 +298,9 @@ export class AssetService {
if (dto.userId) {
const userId = dto.userId;
await this.access.requirePermission(authUser, Permission.TIMELINE_DOWNLOAD, userId);
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId));
return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
);
}
throw new BadRequestException('assetIds, albumId, or userId is required');
@ -303,13 +329,121 @@ export class AssetService {
return mapAsset(asset);
}
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, ...options } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
await this.assetRepository.updateAll(ids, options);
}
async handleAssetDeletionCheck() {
const config = await this.configCore.getConfig();
const trashedDays = config.trash.enabled ? config.trash.days : 0;
const trashedBefore = DateTime.now()
.minus(Duration.fromObject({ days: trashedDays }))
.toJSDate();
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getAll(pagination, { trashedBefore }),
);
for await (const assets of assetPagination) {
for (const asset of assets) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.id } });
}
}
return true;
}
async handleAssetDeletion(job: IAssetDeletionJob) {
const { id, fromExternal } = job;
const asset = await this.assetRepository.getById(id);
if (!asset) {
return false;
}
// Ignore requests that are not from external library job but is for an external asset
if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) {
return false;
}
if (asset.faces) {
await Promise.all(
asset.faces.map(({ assetId, personId }) =>
this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId } }),
),
);
}
await this.assetRepository.remove(asset);
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [asset.id] } });
this.communicationRepository.send(CommunicationEvent.ASSET_DELETE, asset.ownerId, id);
// TODO refactor this to use cascades
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
}
const files = [asset.webpPath, asset.resizePath, asset.encodedVideoPath, asset.sidecarPath];
if (!fromExternal) {
files.push(asset.originalPath);
}
if (!asset.isReadOnly) {
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
}
return true;
}
async deleteAll(authUser: AuthUserDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_DELETE, ids);
if (force) {
for (const id of ids) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id } });
}
} else {
await this.assetRepository.softDeleteAll(ids);
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids } });
this.communicationRepository.send(CommunicationEvent.ASSET_TRASH, authUser.id, ids);
}
}
async handleTrashAction(authUser: AuthUserDto, action: TrashAction): Promise<void> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, authUser.id, { trashedBefore: DateTime.now().toJSDate() }),
);
if (action == TrashAction.RESTORE_ALL) {
for await (const assets of assetPagination) {
const ids = assets.map((a) => a.id);
await this.assetRepository.restoreAll(ids);
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
}
return;
}
if (action == TrashAction.EMPTY_ALL) {
for await (const assets of assetPagination) {
for (const asset of assets) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.id } });
}
}
return;
}
}
async restoreAll(authUser: AuthUserDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_RESTORE, ids);
await this.assetRepository.restoreAll(ids);
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
}
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);

View file

@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
import { AssetStats } from '../asset.repository';
import { AssetStats } from '../../repositories';
export class AssetStatsDto {
@IsBoolean()
@ -15,6 +15,11 @@ export class AssetStatsDto {
@Transform(toBoolean)
@Optional()
isFavorite?: boolean;
@IsBoolean()
@Transform(toBoolean)
@Optional()
isTrashed?: boolean;
}
export class AssetStatsResponseDto {

View file

@ -34,3 +34,14 @@ export class RandomAssetsDto {
@Type(() => Number)
count?: number;
}
export enum TrashAction {
EMPTY_ALL = 'empty-all',
RESTORE_ALL = 'restore-all',
}
export class AssetBulkDeleteDto extends BulkIdsDto {
@Optional()
@IsBoolean()
force?: boolean;
}

View file

@ -1,14 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDate, IsNumber, IsPositive } from 'class-validator';
import { IsInt, Max, Min } from 'class-validator';
export class MemoryLaneDto {
/** Get pictures for +24 hours from this time going back x years */
@IsDate()
@Type(() => Date)
timestamp!: Date;
@IsNumber()
@IsPositive()
@IsInt()
@Type(() => Number)
years = 30;
@Max(31)
@Min(1)
@ApiProperty({ type: 'integer' })
day!: number;
@IsInt()
@Type(() => Number)
@Max(12)
@Min(1)
@ApiProperty({ type: 'integer' })
month!: number;
}

View file

@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { Optional, ValidateUUID, toBoolean } from '../../domain.util';
import { TimeBucketSize } from '../asset.repository';
import { TimeBucketSize } from '../../repositories';
export class TimeBucketDto {
@IsNotEmpty()
@ -28,6 +28,11 @@ export class TimeBucketDto {
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean;
@Optional()
@IsBoolean()
@Transform(toBoolean)
isTrashed?: boolean;
}
export class TimeBucketAssetDto extends TimeBucketDto {

View file

@ -1,4 +1,3 @@
export * from './asset.repository';
export * from './asset.service';
export * from './dto';
export * from './response-dto';

View file

@ -26,6 +26,8 @@ export class AssetResponseDto {
updatedAt!: Date;
isFavorite!: boolean;
isArchived!: boolean;
isTrashed!: boolean;
localDateTime!: Date;
isOffline!: boolean;
isExternal!: boolean;
isReadOnly!: boolean;
@ -54,9 +56,11 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
localDateTime: entity.localDateTime,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite,
isArchived: entity.isArchived,
isTrashed: !!entity.deletedAt,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: withExif ? (entity.exifInfo ? mapExif(entity.exifInfo) : undefined) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,

View file

@ -1,6 +1,6 @@
import { DatabaseAction, EntityType } from '@app/infra/entities';
import { IAccessRepositoryMock, auditStub, authStub, newAccessRepositoryMock, newAuditRepositoryMock } from '@test';
import { IAuditRepository } from './audit.repository';
import { IAuditRepository } from '../repositories';
import { AuditService } from './audit.service';
describe(AuditService.name, () => {

View file

@ -1,11 +1,11 @@
import { DatabaseAction } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AccessCore, IAccessRepository, Permission } from '../access';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
import { IAccessRepository, IAuditRepository } from '../repositories';
import { AuditDeletesDto, AuditDeletesResponseDto } from './audit.dto';
import { IAuditRepository } from './audit.repository';
@Injectable()
export class AuditService {

View file

@ -1,3 +1,2 @@
export * from './audit.dto';
export * from './audit.repository';
export * from './audit.service';

View file

@ -19,16 +19,18 @@ import {
import { IncomingHttpHeaders } from 'http';
import { Issuer, generators } from 'openid-client';
import { Socket } from 'socket.io';
import { IKeyRepository } from '../api-key';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { ILibraryRepository } from '../library';
import { ISharedLinkRepository } from '../shared-link';
import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import {
ICryptoRepository,
IKeyRepository,
ILibraryRepository,
ISharedLinkRepository,
ISystemConfigRepository,
IUserRepository,
IUserTokenRepository,
} from '../repositories';
import { AuthType } from './auth.constant';
import { AuthService } from './auth.service';
import { AuthUserDto, SignUpDto } from './dto';
import { IUserTokenRepository } from './user-token.repository';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');

Some files were not shown because too many files have changed in this diff Show more