mirror of
https://github.com/immich-app/immich.git
synced 2025-12-07 05:11:00 -08:00
Compare commits
41 Commits
v1.83.0
...
dev/better
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b49d9660f6 | ||
|
|
b58edae134 | ||
|
|
2b9f20a1b5 | ||
|
|
d5f8199655 | ||
|
|
d8903de92e | ||
|
|
1d35965d03 | ||
|
|
309bf1ad22 | ||
|
|
0130591a0f | ||
|
|
cf4ec06750 | ||
|
|
e8712e6694 | ||
|
|
ce5966c23d | ||
|
|
68f6446718 | ||
|
|
197f336b5f | ||
|
|
cd375a976e | ||
|
|
088d5addf2 | ||
|
|
2377df9dae | ||
|
|
ad5ba82f50 | ||
|
|
b6f18cbe81 | ||
|
|
87a0ba3db3 | ||
|
|
3212a47720 | ||
|
|
431536cdbb | ||
|
|
9a60578088 | ||
|
|
8dcd159bd6 | ||
|
|
2f87463170 | ||
|
|
9f56bf0ab9 | ||
|
|
603b056512 | ||
|
|
ce04e9e07a | ||
|
|
c54a188154 | ||
|
|
c77ba46d60 | ||
|
|
cc3149c520 | ||
|
|
512f672e9e | ||
|
|
b117985f66 | ||
|
|
b92a2b2a56 | ||
|
|
a6f39bc74f | ||
|
|
daad02504f | ||
|
|
8a6889529c | ||
|
|
b34cbd881a | ||
|
|
f6eaaab725 | ||
|
|
2a2c74e081 | ||
|
|
c653e0f261 | ||
|
|
f0dd1d715a |
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -166,7 +166,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev
|
||||
poetry run pip install --no-deps -r requirements.txt
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
poetry run ruff check --format=github app
|
||||
|
||||
11
README.md
11
README.md
@@ -66,7 +66,7 @@ password: demo
|
||||
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
# Features
|
||||
## Features
|
||||
|
||||
| Features | Mobile | Web |
|
||||
| -------------------------------------------- | ------ | --- |
|
||||
@@ -96,7 +96,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
| Offline support | Yes | No |
|
||||
| Read-only gallery | Yes | Yes |
|
||||
|
||||
# Support the project
|
||||
## Support the project
|
||||
|
||||
I've committed to this project, and I will not stop. I will keep updating the docs, adding new features, and fixing bugs. But I can't do it alone. So I need your help to give me additional motivation to keep going.
|
||||
|
||||
@@ -104,10 +104,15 @@ As our hosts in the [selfhosted.show - In the episode 'The-organization-must-not
|
||||
|
||||
If you feel like this is the right cause and the app is something you are seeing yourself using for a long time, please consider supporting the project with the option below.
|
||||
|
||||
## Donation
|
||||
### Donation
|
||||
|
||||
- [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
|
||||
- [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
|
||||
775
cli/src/api/open-api/api.ts
generated
775
cli/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.83.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -99,6 +99,103 @@ export interface APIKeyUpdateDto {
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityCreateDto
|
||||
*/
|
||||
export interface ActivityCreateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'albumId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'assetId'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'comment'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'type': ReactionType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityResponseDto
|
||||
*/
|
||||
export interface ActivityResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'assetId': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'comment'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'type': ActivityResponseDtoTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {UserDto}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'user': UserDto;
|
||||
}
|
||||
|
||||
export const ActivityResponseDtoTypeEnum = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ActivityResponseDtoTypeEnum = typeof ActivityResponseDtoTypeEnum[keyof typeof ActivityResponseDtoTypeEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityStatisticsResponseDto
|
||||
*/
|
||||
export interface ActivityStatisticsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ActivityStatisticsResponseDto
|
||||
*/
|
||||
'comments': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -2490,6 +2587,20 @@ export interface QueueStatusDto {
|
||||
*/
|
||||
'isPaused': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const ReactionType = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ReactionType = typeof ReactionType[keyof typeof ReactionType];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -3038,6 +3149,12 @@ export interface SharedLinkCreateDto {
|
||||
* @memberof SharedLinkCreateDto
|
||||
*/
|
||||
'expiresAt'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkCreateDto
|
||||
*/
|
||||
'password'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -3089,6 +3206,12 @@ export interface SharedLinkEditDto {
|
||||
* @memberof SharedLinkEditDto
|
||||
*/
|
||||
'expiresAt'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkEditDto
|
||||
*/
|
||||
'password'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -3156,12 +3279,24 @@ export interface SharedLinkResponseDto {
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'key': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'password': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'showMetadata': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'token'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {SharedLinkType}
|
||||
@@ -3259,6 +3394,12 @@ export interface SystemConfigDto {
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'job': SystemConfigJobDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigLibraryDto}
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'library': SystemConfigLibraryDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigMachineLearningDto}
|
||||
@@ -3510,6 +3651,38 @@ export interface SystemConfigJobDto {
|
||||
*/
|
||||
'videoConversion': JobSettingsDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SystemConfigLibraryDto
|
||||
*/
|
||||
export interface SystemConfigLibraryDto {
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigLibraryScanDto}
|
||||
* @memberof SystemConfigLibraryDto
|
||||
*/
|
||||
'scan': SystemConfigLibraryScanDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SystemConfigLibraryScanDto
|
||||
*/
|
||||
export interface SystemConfigLibraryScanDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SystemConfigLibraryScanDto
|
||||
*/
|
||||
'cronExpression': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SystemConfigLibraryScanDto
|
||||
*/
|
||||
'enabled': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -3940,6 +4113,7 @@ export const TranscodeHWAccel = {
|
||||
Nvenc: 'nvenc',
|
||||
Qsv: 'qsv',
|
||||
Vaapi: 'vaapi',
|
||||
Rkmpp: 'rkmpp',
|
||||
Disabled: 'disabled'
|
||||
} as const;
|
||||
|
||||
@@ -4188,15 +4362,39 @@ export interface UsageByUserDto {
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserCountResponseDto
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserCountResponseDto {
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UserCountResponseDto
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'userCount': number;
|
||||
'email': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'firstName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'lastName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -4781,6 +4979,448 @@ export class APIKeyApi extends BaseAPI {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ActivityApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity: async (activityCreateDto: ActivityCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'activityCreateDto' is not null or undefined
|
||||
assertParamExists('createActivity', 'activityCreateDto', activityCreateDto)
|
||||
const localVarPath = `/activity`;
|
||||
// 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(activityCreateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteActivity', 'id', id)
|
||||
const localVarPath = `/activity/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...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 {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {string} [userId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivities', 'albumId', albumId)
|
||||
const localVarPath = `/activity`;
|
||||
// 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 (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
localVarQueryParameter['type'] = type;
|
||||
}
|
||||
|
||||
if (userId !== undefined) {
|
||||
localVarQueryParameter['userId'] = userId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics: async (albumId: string, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivityStatistics', 'albumId', albumId)
|
||||
const localVarPath = `/activity/statistics`;
|
||||
// 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 (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createActivity(activityCreateDto: ActivityCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(activityCreateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteActivity(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteActivity(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {string} [userId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivityStatistics(albumId: string, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityStatisticsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivityStatistics(albumId, assetId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ActivityApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityResponseDto> {
|
||||
return localVarFp.createActivity(requestParameters.activityCreateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteActivity(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
|
||||
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityStatisticsResponseDto> {
|
||||
return localVarFp.getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiCreateActivityRequest
|
||||
*/
|
||||
export interface ActivityApiCreateActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {ActivityCreateDto}
|
||||
* @memberof ActivityApiCreateActivity
|
||||
*/
|
||||
readonly activityCreateDto: ActivityCreateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for deleteActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiDeleteActivityRequest
|
||||
*/
|
||||
export interface ActivityApiDeleteActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiDeleteActivity
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivities operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivitiesRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivitiesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly assetId?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly type?: ReactionType
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly userId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivityStatistics operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivityStatisticsRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivityStatisticsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly assetId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityApi - object-oriented interface
|
||||
* @export
|
||||
* @class ActivityApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ActivityApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).createActivity(requestParameters.activityCreateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).deleteActivity(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AlbumApi - axios parameter creator
|
||||
* @export
|
||||
@@ -13690,11 +14330,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [password]
|
||||
* @param {string} [token]
|
||||
* @param {string} [key]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getMySharedLink: async (key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
getMySharedLink: async (password?: string, token?: string, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/shared-link/me`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@@ -13716,6 +14358,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (password !== undefined) {
|
||||
localVarQueryParameter['password'] = password;
|
||||
}
|
||||
|
||||
if (token !== undefined) {
|
||||
localVarQueryParameter['token'] = token;
|
||||
}
|
||||
|
||||
if (key !== undefined) {
|
||||
localVarQueryParameter['key'] = key;
|
||||
}
|
||||
@@ -13959,12 +14609,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [password]
|
||||
* @param {string} [token]
|
||||
* @param {string} [key]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options);
|
||||
async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@@ -14053,7 +14705,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.getMySharedLink(requestParameters.key, options).then((request) => request(axios, basePath));
|
||||
return localVarFp.getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -14142,6 +14794,20 @@ export interface SharedLinkApiCreateSharedLinkRequest {
|
||||
* @interface SharedLinkApiGetMySharedLinkRequest
|
||||
*/
|
||||
export interface SharedLinkApiGetMySharedLinkRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkApiGetMySharedLink
|
||||
*/
|
||||
readonly password?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkApiGetMySharedLink
|
||||
*/
|
||||
readonly token?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -14274,7 +14940,7 @@ export class SharedLinkApi extends BaseAPI {
|
||||
* @memberof SharedLinkApi
|
||||
*/
|
||||
public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) {
|
||||
return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||
return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15692,49 +16358,6 @@ export const UserApiAxiosParamCreator = 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 {boolean} [admin]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getUserCount: async (admin?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/user/count`;
|
||||
// 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 (admin !== undefined) {
|
||||
localVarQueryParameter['admin'] = admin;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -15909,16 +16532,6 @@ export const UserApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserById(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [admin]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getUserCount(admin?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserCountResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(admin, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -16011,15 +16624,6 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
||||
getUserById(requestParameters: UserApiGetUserByIdRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
||||
return localVarFp.getUserById(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UserApiGetUserCountRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getUserCount(requestParameters: UserApiGetUserCountRequest = {}, options?: AxiosRequestConfig): AxiosPromise<UserCountResponseDto> {
|
||||
return localVarFp.getUserCount(requestParameters.admin, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UserApiRestoreUserRequest} requestParameters Request parameters.
|
||||
@@ -16125,20 +16729,6 @@ export interface UserApiGetUserByIdRequest {
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getUserCount operation in UserApi.
|
||||
* @export
|
||||
* @interface UserApiGetUserCountRequest
|
||||
*/
|
||||
export interface UserApiGetUserCountRequest {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof UserApiGetUserCount
|
||||
*/
|
||||
readonly admin?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for restoreUser operation in UserApi.
|
||||
* @export
|
||||
@@ -16250,17 +16840,6 @@ export class UserApi extends BaseAPI {
|
||||
return UserApiFp(this.configuration).getUserById(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UserApiGetUserCountRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UserApi
|
||||
*/
|
||||
public getUserCount(requestParameters: UserApiGetUserCountRequest = {}, options?: AxiosRequestConfig) {
|
||||
return UserApiFp(this.configuration).getUserCount(requestParameters.admin, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UserApiRestoreUserRequest} requestParameters Request parameters.
|
||||
|
||||
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.83.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.83.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.83.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.83.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
24
docker/hwaccel-rkmpp.yml
Normal file
24
docker/hwaccel-rkmpp.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: "3.8"
|
||||
|
||||
# Hardware acceleration for transcoding using RKMPP for Rockchip SOCs
|
||||
# This is only needed if you want to use hardware acceleration for transcoding.
|
||||
# Supported host OS is Ubuntu Jammy 22.04 with custom ffmpeg from ppa:liujianfeng1994/rockchip-multimedia
|
||||
|
||||
services:
|
||||
hwaccel:
|
||||
security_opt: # enables full access to /sys and /proc, still far better than privileged: true
|
||||
- systempaths=unconfined
|
||||
- apparmor=unconfined
|
||||
group_add:
|
||||
- video
|
||||
devices:
|
||||
- /dev/rga:/dev/rga
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dma_heap:/dev/dma_heap
|
||||
- /dev/mpp_service:/dev/mpp_service
|
||||
volumes:
|
||||
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
||||
@@ -33,8 +33,6 @@ To be concise, Immich can now read in the gallery files, register the path into
|
||||
- Only new files that are added to the gallery will be detected.
|
||||
- Deleted and moved files will not be detected.
|
||||
|
||||
You can find more information on how to use the feature by reading the documentation [here](/docs/features/read-only-gallery).
|
||||
|
||||
## Memory feature
|
||||
|
||||
This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features.
|
||||
|
||||
@@ -12,6 +12,6 @@ The backend has an end-to-end test suite that can be called with `npm run test:e
|
||||
|
||||
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.
|
||||
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 perform 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`.
|
||||
|
||||
@@ -32,7 +32,6 @@ immich
|
||||
| --server / -s | Immich's server address |
|
||||
| --threads / -t | Number of threads to use (Default 5) |
|
||||
| --album/ -al | Create albums for assets based on the parent folder or a given name |
|
||||
| --import/ -i | Import gallery (assets are not uploaded) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -108,70 +107,3 @@ npm run build
|
||||
```bash title="Run the command"
|
||||
node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Importing existing libraries
|
||||
|
||||
If you do not wish to upload files into the server, existing files can be imported into the immich gallery through the use of the `--import` flag.
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/ --import
|
||||
```
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg --import
|
||||
```
|
||||
|
||||
The `immich-server` and `immich-microservices` containers must be able to access the files, or directories at the path referenced in the command. The directories referenced must be set under a user's `External Path` setting. More detailed instructions can be found [here](/docs/features/read-only-gallery).
|
||||
|
||||
:::tip Matching volume references
|
||||
The import command is most easily run on the machine running the immich service, as the path to the files on the machine running the command and the server much match identically.
|
||||
|
||||
If you are running immich within docker, the volume pointing to your existing library should be identical with your host machine.
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /path/to/media:/path/to/media
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "microservices" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /path/to/media:/path/to/media
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
```
|
||||
|
||||
The proper command for above would be as shown below. You should have access to `/path/to/media` exactly on the environment the CLI command is being run on
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
|
||||
```
|
||||
|
||||
If you are running the import using the docker command, please note that the volumes should point to the `/path/to/media` exactly on the environment the CLI command is being run on
|
||||
|
||||
```
|
||||
docker run -it --rm -v "/path/to/media:/path/to/media" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@@ -85,7 +85,7 @@ There is an automatic job that's run once a day and refreshes all modified files
|
||||
|
||||
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
|
||||
|
||||
- `/home/user/old-pics`: a folder contining childhood photos.
|
||||
- `/home/user/old-pics`: a folder containing childhood photos.
|
||||
- `/mnt/nas/christmas-trip`: photos from a christmas trip. The subfolder `/mnt/nas/christmas-trip/Raw` contains the raw files directly from the DSLR. We don't want to import the raw files to Immich
|
||||
- `/mnt/media/videos`: Videos from the same christmas trip.
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# Read-only Gallery [Deprecated]
|
||||
|
||||
:::caution
|
||||
|
||||
This feature is being deprecated in favor of [Libraries](/docs/features/libraries.md).
|
||||
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
This feature enables users to use an existing gallery without uploading the assets to Immich.
|
||||
|
||||
Upon syncing the file information, it will be read by Immich to generate supported files.
|
||||
|
||||
## Usage
|
||||
|
||||
:::tip Example scenario
|
||||
|
||||
On the VM/system that Immich is running, I have 2 galleries that I want to use with Immich.
|
||||
|
||||
- My gallery is stored at `/mnt/media/precious-memory`
|
||||
- My wife's gallery is stored at `/mnt/media/childhood-memory`
|
||||
|
||||
We will use those values in the steps below.
|
||||
|
||||
:::
|
||||
|
||||
### Mount the gallery to the containers.
|
||||
|
||||
`immich-server` and `immich-microservices` containers will need access to the gallery. Mount the directory path as in the example below
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
|
||||
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "microservices" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
|
||||
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
```
|
||||
|
||||
:::tip
|
||||
Internal and external path have to be identical.
|
||||
:::
|
||||
|
||||
_Remember to bring the container down/up to register the changes. Make sure you can see the mounted path in the container._
|
||||
|
||||
### Register the path for the user.
|
||||
|
||||
This action is done by the admin of the instance.
|
||||
|
||||
- Navigate to `Administration > Users` page on the web.
|
||||
- Click on the user edit button.
|
||||
- Add the gallery path to the `External Path` field for the corresponding user and confirm the changes.
|
||||
|
||||
<img src={require('./img/me.png').default} width='33%' title='My Account Storage Path' />
|
||||
|
||||
<img src={require('./img/my-wife.png').default} width='33%' title='My Wifes Account Storage Path' />
|
||||
|
||||
### Sync with the CLI tool.
|
||||
|
||||
- Install or update the [CLI Tool](/docs/features/bulk-upload.md). The import feature is supported from version `v0.39.0` of the CLI
|
||||
- Run the command below to sync the gallery with Immich.
|
||||
|
||||
```bash title="Import my gallery"
|
||||
immich upload --key <my-api-key> --server http://my-server-ip:2283/api /mnt/media/precious-memory --recursive --import
|
||||
```
|
||||
|
||||
```bash title="Import my wife gallery"
|
||||
immich upload --key <my-wife-api-key> --server http://my-server-ip:2283/api /mnt/media/childhood-memory --recursive --import
|
||||
```
|
||||
|
||||
The `--import` flag will tell Immich to import the files by path instead of uploading them.
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
mdiCheckAll,
|
||||
mdiCheckboxMarked,
|
||||
mdiCollage,
|
||||
mdiContentCopy,
|
||||
mdiDevices,
|
||||
mdiFaceMan,
|
||||
mdiFaceManOutline,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
mdiMerge,
|
||||
mdiMonitor,
|
||||
mdiMotionPlayOutline,
|
||||
mdiPalette,
|
||||
mdiPanVertical,
|
||||
mdiPartyPopper,
|
||||
mdiRaw,
|
||||
@@ -47,6 +49,33 @@ import React from 'react';
|
||||
import Timeline, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 20K Stars on GitHub!',
|
||||
title: '20,000 Stars',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiContentCopy,
|
||||
title: 'Stack assets',
|
||||
description: 'Manual asset stacking for grouping and hiding related assets in the main timeline.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiPalette,
|
||||
title: 'Custom theme',
|
||||
description: 'Apply your custom CSS for modifying fonts, colors, and styles in the web application.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiTrashCanOutline,
|
||||
title: 'Trash Feature',
|
||||
@@ -283,7 +312,7 @@ const items: Item[] = [
|
||||
},
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 10K Starts on GitHub!',
|
||||
description: 'Reach 10K Stars on GitHub!',
|
||||
title: '10,000 Stars',
|
||||
release: 'v1.54.0',
|
||||
tag: 'v1.54.0',
|
||||
|
||||
@@ -10,9 +10,8 @@ RUN poetry config installer.max-workers 10 && \
|
||||
RUN python -m venv /opt/venv
|
||||
ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
COPY poetry.lock pyproject.toml requirements.txt ./
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
RUN pip install --no-deps -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
from typing import Any, Iterator, TypeAlias
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
@@ -8,8 +9,7 @@ from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
from .main import app, init_state
|
||||
|
||||
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
|
||||
from .schemas import ndarray_f32
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,13 +18,13 @@ def pil_image() -> Image.Image:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cv_image(pil_image: Image.Image) -> ndarray:
|
||||
def cv_image(pil_image: Image.Image) -> ndarray_f32:
|
||||
return np.asarray(pil_image)[:, :, ::-1] # PIL uses RGB while cv2 uses BGR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_model() -> Iterator[mock.Mock]:
|
||||
with mock.patch("app.models.cache.InferenceModel.from_model_type", autospec=True) as mocked:
|
||||
with mock.patch("app.models.cache.from_model_type", autospec=True) as mocked:
|
||||
yield mocked
|
||||
|
||||
|
||||
@@ -37,3 +37,25 @@ def deployed_app() -> TestClient:
|
||||
@pytest.fixture(scope="session")
|
||||
def responses() -> dict[str, Any]:
|
||||
return json.load(open("responses.json", "r"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_model_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"embed_dim": 512,
|
||||
"vision_cfg": {"image_size": 224, "layers": 12, "width": 768, "patch_size": 32},
|
||||
"text_cfg": {"context_length": 77, "vocab_size": 49408, "width": 512, "heads": 8, "layers": 12},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_preprocess_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"size": [224, 224],
|
||||
"mode": "RGB",
|
||||
"mean": [0.48145466, 0.4578275, 0.40821073],
|
||||
"std": [0.26862954, 0.26130258, 0.27577711],
|
||||
"interpolation": "bicubic",
|
||||
"resize_mode": "shortest",
|
||||
"fill_color": 0,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
from .clip import CLIPEncoder
|
||||
from typing import Any
|
||||
|
||||
from app.schemas import ModelType
|
||||
|
||||
from .base import InferenceModel
|
||||
from .clip import MCLIPEncoder, OpenCLIPEncoder, is_mclip, is_openclip
|
||||
from .facial_recognition import FaceRecognizer
|
||||
from .image_classification import ImageClassifier
|
||||
|
||||
|
||||
def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any) -> InferenceModel:
|
||||
match model_type:
|
||||
case ModelType.CLIP:
|
||||
if is_openclip(model_name):
|
||||
return OpenCLIPEncoder(model_name, **model_kwargs)
|
||||
elif is_mclip(model_name):
|
||||
return MCLIPEncoder(model_name, **model_kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown CLIP model {model_name}")
|
||||
case ModelType.FACIAL_RECOGNITION:
|
||||
return FaceRecognizer(model_name, **model_kwargs)
|
||||
case ModelType.IMAGE_CLASSIFICATION:
|
||||
return ImageClassifier(model_name, **model_kwargs)
|
||||
case _:
|
||||
raise ValueError(f"Unknown model type {model_type}")
|
||||
|
||||
@@ -25,7 +25,7 @@ class InferenceModel(ABC):
|
||||
) -> None:
|
||||
self.model_name = model_name
|
||||
self.loaded = False
|
||||
self._cache_dir = Path(cache_dir) if cache_dir is not None else get_cache_dir(model_name, self.model_type)
|
||||
self._cache_dir = Path(cache_dir) if cache_dir is not None else None
|
||||
self.providers = model_kwargs.pop("providers", ["CPUExecutionProvider"])
|
||||
# don't pre-allocate more memory than needed
|
||||
self.provider_options = model_kwargs.pop(
|
||||
@@ -92,7 +92,7 @@ class InferenceModel(ABC):
|
||||
|
||||
@property
|
||||
def cache_dir(self) -> Path:
|
||||
return self._cache_dir
|
||||
return self._cache_dir if self._cache_dir is not None else get_cache_dir(self.model_name, self.model_type)
|
||||
|
||||
@cache_dir.setter
|
||||
def cache_dir(self, cache_dir: Path) -> None:
|
||||
|
||||
@@ -4,6 +4,8 @@ from aiocache.backends.memory import SimpleMemoryCache
|
||||
from aiocache.lock import OptimisticLock
|
||||
from aiocache.plugins import BasePlugin, TimingPlugin
|
||||
|
||||
from app.models import from_model_type
|
||||
|
||||
from ..schemas import ModelType
|
||||
from .base import InferenceModel
|
||||
|
||||
@@ -50,7 +52,7 @@ class ModelCache:
|
||||
async with OptimisticLock(self.cache, key) as lock:
|
||||
model = await self.cache.get(key)
|
||||
if model is None:
|
||||
model = InferenceModel.from_model_type(model_type, model_name, **model_kwargs)
|
||||
model = from_model_type(model_type, model_name, **model_kwargs)
|
||||
await lock.cas(model, ttl=self.ttl)
|
||||
return model
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import os
|
||||
import zipfile
|
||||
import json
|
||||
from abc import abstractmethod
|
||||
from functools import cached_property
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import torch
|
||||
from clip_server.model.clip import BICUBIC, _convert_image_to_rgb
|
||||
from clip_server.model.clip_onnx import _MODELS, _S3_BUCKET_V2, CLIPOnnxModel, download_model
|
||||
from clip_server.model.pretrained_models import _VISUAL_MODEL_IMAGE_SIZE
|
||||
from clip_server.model.tokenization import Tokenizer
|
||||
from huggingface_hub import snapshot_download
|
||||
from PIL import Image
|
||||
from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToTensor
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from app.config import log
|
||||
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
|
||||
from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64
|
||||
|
||||
from ..config import log
|
||||
from ..schemas import ModelType
|
||||
from .base import InferenceModel
|
||||
|
||||
|
||||
class CLIPEncoder(InferenceModel):
|
||||
class BaseCLIPEncoder(InferenceModel):
|
||||
_model_type = ModelType.CLIP
|
||||
|
||||
def __init__(
|
||||
@@ -27,48 +28,29 @@ class CLIPEncoder(InferenceModel):
|
||||
mode: Literal["text", "vision"] | None = None,
|
||||
**model_kwargs: Any,
|
||||
) -> 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 model_name not in _MODELS:
|
||||
raise ValueError(f"Unknown model name {model_name}.")
|
||||
self.mode = mode
|
||||
super().__init__(model_name, cache_dir, **model_kwargs)
|
||||
|
||||
def _download(self) -> None:
|
||||
models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
|
||||
text_onnx_path = self.cache_dir / "textual.onnx"
|
||||
vision_onnx_path = self.cache_dir / "visual.onnx"
|
||||
|
||||
if not text_onnx_path.is_file():
|
||||
self._download_model(*models[0])
|
||||
|
||||
if not vision_onnx_path.is_file():
|
||||
self._download_model(*models[1])
|
||||
|
||||
def _load(self) -> None:
|
||||
if self.mode == "text" or self.mode is None:
|
||||
log.debug(f"Loading clip text model '{self.model_name}'")
|
||||
|
||||
self.text_model = ort.InferenceSession(
|
||||
self.cache_dir / "textual.onnx",
|
||||
self.textual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
self.text_outputs = [output.name for output in self.text_model.get_outputs()]
|
||||
self.tokenizer = Tokenizer(self.model_name)
|
||||
|
||||
if self.mode == "vision" or self.mode is None:
|
||||
log.debug(f"Loading clip vision model '{self.model_name}'")
|
||||
|
||||
self.vision_model = ort.InferenceSession(
|
||||
self.cache_dir / "visual.onnx",
|
||||
self.visual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
self.vision_outputs = [output.name for output in self.vision_model.get_outputs()]
|
||||
|
||||
image_size = _VISUAL_MODEL_IMAGE_SIZE[CLIPOnnxModel.get_model_name(self.model_name)]
|
||||
self.transform = _transform_pil_image(image_size)
|
||||
|
||||
def _predict(self, image_or_text: Image.Image | str) -> list[float]:
|
||||
if isinstance(image_or_text, bytes):
|
||||
@@ -78,55 +60,163 @@ class CLIPEncoder(InferenceModel):
|
||||
case Image.Image():
|
||||
if self.mode == "text":
|
||||
raise TypeError("Cannot encode image as text-only model")
|
||||
pixel_values = self.transform(image_or_text)
|
||||
assert isinstance(pixel_values, torch.Tensor)
|
||||
pixel_values = torch.unsqueeze(pixel_values, 0).numpy()
|
||||
outputs = self.vision_model.run(self.vision_outputs, {"pixel_values": pixel_values})
|
||||
|
||||
outputs = self.vision_model.run(None, self.transform(image_or_text))
|
||||
case str():
|
||||
if self.mode == "vision":
|
||||
raise TypeError("Cannot encode text as vision-only model")
|
||||
text_inputs: dict[str, torch.Tensor] = self.tokenizer(image_or_text)
|
||||
inputs = {
|
||||
"input_ids": text_inputs["input_ids"].int().numpy(),
|
||||
"attention_mask": text_inputs["attention_mask"].int().numpy(),
|
||||
}
|
||||
outputs = self.text_model.run(self.text_outputs, inputs)
|
||||
|
||||
outputs = self.text_model.run(None, self.tokenize(image_or_text))
|
||||
case _:
|
||||
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
|
||||
|
||||
return outputs[0][0].tolist()
|
||||
|
||||
def _download_model(self, model_name: str, model_md5: str) -> bool:
|
||||
# downloading logic is adapted from clip-server's CLIPOnnxModel class
|
||||
download_model(
|
||||
url=_S3_BUCKET_V2 + model_name,
|
||||
target_folder=self.cache_dir.as_posix(),
|
||||
md5sum=model_md5,
|
||||
with_resume=True,
|
||||
)
|
||||
file = self.cache_dir / model_name.split("/")[1]
|
||||
if file.suffix == ".zip":
|
||||
with zipfile.ZipFile(file, "r") as zip_ref:
|
||||
zip_ref.extractall(self.cache_dir)
|
||||
os.remove(file)
|
||||
return True
|
||||
@abstractmethod
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def transform(self, image: Image.Image) -> dict[str, ndarray_f32]:
|
||||
pass
|
||||
|
||||
@property
|
||||
def textual_dir(self) -> Path:
|
||||
return self.cache_dir / "textual"
|
||||
|
||||
@property
|
||||
def visual_dir(self) -> Path:
|
||||
return self.cache_dir / "visual"
|
||||
|
||||
@property
|
||||
def model_cfg_path(self) -> Path:
|
||||
return self.cache_dir / "config.json"
|
||||
|
||||
@property
|
||||
def textual_path(self) -> Path:
|
||||
return self.textual_dir / "model.onnx"
|
||||
|
||||
@property
|
||||
def visual_path(self) -> Path:
|
||||
return self.visual_dir / "model.onnx"
|
||||
|
||||
@property
|
||||
def preprocess_cfg_path(self) -> Path:
|
||||
return self.visual_dir / "preprocess_cfg.json"
|
||||
|
||||
@property
|
||||
def cached(self) -> bool:
|
||||
return (self.cache_dir / "textual.onnx").is_file() and (self.cache_dir / "visual.onnx").is_file()
|
||||
return self.textual_path.is_file() and self.visual_path.is_file()
|
||||
|
||||
|
||||
# same as `_transform_blob` without `_blob2image`
|
||||
def _transform_pil_image(n_px: int) -> Compose:
|
||||
return Compose(
|
||||
[
|
||||
Resize(n_px, interpolation=BICUBIC),
|
||||
CenterCrop(n_px),
|
||||
_convert_image_to_rgb,
|
||||
ToTensor(),
|
||||
Normalize(
|
||||
(0.48145466, 0.4578275, 0.40821073),
|
||||
(0.26862954, 0.26130258, 0.27577711),
|
||||
),
|
||||
]
|
||||
)
|
||||
class OpenCLIPEncoder(BaseCLIPEncoder):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str | None = None,
|
||||
mode: Literal["text", "vision"] | None = None,
|
||||
**model_kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(_clean_model_name(model_name), cache_dir, mode, **model_kwargs)
|
||||
|
||||
def _download(self) -> None:
|
||||
snapshot_download(
|
||||
f"immich-app/{self.model_name}",
|
||||
cache_dir=self.cache_dir,
|
||||
local_dir=self.cache_dir,
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
|
||||
def _load(self) -> None:
|
||||
super()._load()
|
||||
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(self.textual_dir)
|
||||
self.sequence_length = self.model_cfg["text_cfg"]["context_length"]
|
||||
|
||||
self.size = (
|
||||
self.preprocess_cfg["size"][0] if type(self.preprocess_cfg["size"]) == list else self.preprocess_cfg["size"]
|
||||
)
|
||||
self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"])
|
||||
self.mean = np.array(self.preprocess_cfg["mean"], dtype=np.float32)
|
||||
self.std = np.array(self.preprocess_cfg["std"], dtype=np.float32)
|
||||
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
input_ids: ndarray_i64 = self.tokenizer(
|
||||
text,
|
||||
max_length=self.sequence_length,
|
||||
return_tensors="np",
|
||||
return_attention_mask=False,
|
||||
padding="max_length",
|
||||
truncation=True,
|
||||
).input_ids
|
||||
return {"text": input_ids.astype(np.int32)}
|
||||
|
||||
def transform(self, image: Image.Image) -> dict[str, ndarray_f32]:
|
||||
image = resize(image, self.size)
|
||||
image = crop(image, self.size)
|
||||
image_np = to_numpy(image)
|
||||
image_np = normalize(image_np, self.mean, self.std)
|
||||
return {"image": np.expand_dims(image_np.transpose(2, 0, 1), 0)}
|
||||
|
||||
@cached_property
|
||||
def model_cfg(self) -> dict[str, Any]:
|
||||
return json.load(self.model_cfg_path.open())
|
||||
|
||||
@cached_property
|
||||
def preprocess_cfg(self) -> dict[str, Any]:
|
||||
return json.load(self.preprocess_cfg_path.open())
|
||||
|
||||
|
||||
class MCLIPEncoder(OpenCLIPEncoder):
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np")
|
||||
return {k: v.astype(np.int32) for k, v in tokens.items()}
|
||||
|
||||
|
||||
_OPENCLIP_MODELS = {
|
||||
"RN50__openai",
|
||||
"RN50__yfcc15m",
|
||||
"RN50__cc12m",
|
||||
"RN101__openai",
|
||||
"RN101__yfcc15m",
|
||||
"RN50x4__openai",
|
||||
"RN50x16__openai",
|
||||
"RN50x64__openai",
|
||||
"ViT-B-32__openai",
|
||||
"ViT-B-32__laion2b_e16",
|
||||
"ViT-B-32__laion400m_e31",
|
||||
"ViT-B-32__laion400m_e32",
|
||||
"ViT-B-32__laion2b-s34b-b79k",
|
||||
"ViT-B-16__openai",
|
||||
"ViT-B-16__laion400m_e31",
|
||||
"ViT-B-16__laion400m_e32",
|
||||
"ViT-B-16-plus-240__laion400m_e31",
|
||||
"ViT-B-16-plus-240__laion400m_e32",
|
||||
"ViT-L-14__openai",
|
||||
"ViT-L-14__laion400m_e31",
|
||||
"ViT-L-14__laion400m_e32",
|
||||
"ViT-L-14__laion2b-s32b-b82k",
|
||||
"ViT-L-14-336__openai",
|
||||
"ViT-H-14__laion2b-s32b-b79k",
|
||||
"ViT-g-14__laion2b-s12b-b42k",
|
||||
}
|
||||
|
||||
|
||||
_MCLIP_MODELS = {
|
||||
"LABSE-Vit-L-14",
|
||||
"XLM-Roberta-Large-Vit-B-32",
|
||||
"XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"XLM-Roberta-Large-Vit-L-14",
|
||||
}
|
||||
|
||||
|
||||
def _clean_model_name(model_name: str) -> str:
|
||||
return model_name.split("/")[-1].replace("::", "__")
|
||||
|
||||
|
||||
def is_openclip(model_name: str) -> bool:
|
||||
return _clean_model_name(model_name) in _OPENCLIP_MODELS
|
||||
|
||||
|
||||
def is_mclip(model_name: str) -> bool:
|
||||
return _clean_model_name(model_name) in _MCLIP_MODELS
|
||||
|
||||
@@ -9,7 +9,8 @@ from insightface.model_zoo import ArcFaceONNX, RetinaFace
|
||||
from insightface.utils.face_align import norm_crop
|
||||
from insightface.utils.storage import BASE_REPO_URL, download_file
|
||||
|
||||
from ..schemas import ModelType
|
||||
from app.schemas import ModelType, ndarray_f32
|
||||
|
||||
from .base import InferenceModel
|
||||
|
||||
|
||||
@@ -68,7 +69,7 @@ class FaceRecognizer(InferenceModel):
|
||||
)
|
||||
self.rec_model.prepare(ctx_id=0)
|
||||
|
||||
def _predict(self, image: np.ndarray[int, np.dtype[Any]] | bytes) -> list[dict[str, Any]]:
|
||||
def _predict(self, image: ndarray_f32 | bytes) -> list[dict[str, Any]]:
|
||||
if isinstance(image, bytes):
|
||||
image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR)
|
||||
bboxes, kpss = self.det_model.detect(image)
|
||||
|
||||
35
machine-learning/app/models/transforms.py
Normal file
35
machine-learning/app/models/transforms.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from app.schemas import ndarray_f32
|
||||
|
||||
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
|
||||
|
||||
|
||||
def resize(img: Image.Image, size: int) -> Image.Image:
|
||||
if img.width < img.height:
|
||||
return img.resize((size, int((img.height / img.width) * size)), resample=Image.BICUBIC)
|
||||
else:
|
||||
return img.resize((int((img.width / img.height) * size), size), resample=Image.BICUBIC)
|
||||
|
||||
|
||||
# https://stackoverflow.com/a/60883103
|
||||
def crop(img: Image.Image, size: int) -> Image.Image:
|
||||
left = int((img.size[0] / 2) - (size / 2))
|
||||
upper = int((img.size[1] / 2) - (size / 2))
|
||||
right = left + size
|
||||
lower = upper + size
|
||||
|
||||
return img.crop((left, upper, right, lower))
|
||||
|
||||
|
||||
def to_numpy(img: Image.Image) -> ndarray_f32:
|
||||
return np.asarray(img.convert("RGB")).astype(np.float32) / 255.0
|
||||
|
||||
|
||||
def normalize(img: ndarray_f32, mean: float | ndarray_f32, std: float | ndarray_f32) -> ndarray_f32:
|
||||
return (img - mean) / std
|
||||
|
||||
|
||||
def get_pil_resampling(resample: str) -> Image.Resampling:
|
||||
return _PIL_RESAMPLING_METHODS[resample.lower()]
|
||||
@@ -1,5 +1,7 @@
|
||||
from enum import StrEnum
|
||||
from typing import TypeAlias
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -31,3 +33,8 @@ class ModelType(StrEnum):
|
||||
IMAGE_CLASSIFICATION = "image-classification"
|
||||
CLIP = "clip"
|
||||
FACIAL_RECOGNITION = "facial-recognition"
|
||||
|
||||
|
||||
ndarray_f32: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
|
||||
ndarray_i64: TypeAlias = np.ndarray[int, np.dtype[np.int64]]
|
||||
ndarray_i32: TypeAlias = np.ndarray[int, np.dtype[np.int32]]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import pickle
|
||||
from io import BytesIO
|
||||
from typing import Any, TypeAlias
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from unittest import mock
|
||||
|
||||
import cv2
|
||||
@@ -14,13 +15,11 @@ from pytest_mock import MockerFixture
|
||||
from .config import settings
|
||||
from .models.base import PicklableSessionOptions
|
||||
from .models.cache import ModelCache
|
||||
from .models.clip import CLIPEncoder
|
||||
from .models.clip import OpenCLIPEncoder
|
||||
from .models.facial_recognition import FaceRecognizer
|
||||
from .models.image_classification import ImageClassifier
|
||||
from .schemas import ModelType
|
||||
|
||||
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
|
||||
|
||||
|
||||
class TestImageClassifier:
|
||||
classifier_preds = [
|
||||
@@ -56,30 +55,50 @@ class TestImageClassifier:
|
||||
|
||||
class TestCLIP:
|
||||
embedding = np.random.rand(512).astype(np.float32)
|
||||
cache_dir = Path("test_cache")
|
||||
|
||||
def test_basic_image(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
|
||||
mocker.patch.object(CLIPEncoder, "download")
|
||||
def test_basic_image(
|
||||
self,
|
||||
pil_image: Image.Image,
|
||||
mocker: MockerFixture,
|
||||
clip_model_cfg: dict[str, Any],
|
||||
clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
|
||||
) -> None:
|
||||
mocker.patch.object(OpenCLIPEncoder, "download")
|
||||
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
|
||||
mocker.patch("app.models.clip.AutoTokenizer.from_pretrained", autospec=True)
|
||||
mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
|
||||
mocked.return_value.run.return_value = [[self.embedding]]
|
||||
clip_encoder = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="vision")
|
||||
assert clip_encoder.mode == "vision"
|
||||
|
||||
clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="vision")
|
||||
embedding = clip_encoder.predict(pil_image)
|
||||
|
||||
assert clip_encoder.mode == "vision"
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == 512
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
assert all([isinstance(num, float) for num in embedding])
|
||||
clip_encoder.vision_model.run.assert_called_once()
|
||||
|
||||
def test_basic_text(self, mocker: MockerFixture) -> None:
|
||||
mocker.patch.object(CLIPEncoder, "download")
|
||||
def test_basic_text(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
clip_model_cfg: dict[str, Any],
|
||||
clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
|
||||
) -> None:
|
||||
mocker.patch.object(OpenCLIPEncoder, "download")
|
||||
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
|
||||
mocker.patch("app.models.clip.AutoTokenizer.from_pretrained", autospec=True)
|
||||
mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
|
||||
mocked.return_value.run.return_value = [[self.embedding]]
|
||||
clip_encoder = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="text")
|
||||
assert clip_encoder.mode == "text"
|
||||
|
||||
clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="text")
|
||||
embedding = clip_encoder.predict("test search query")
|
||||
|
||||
assert clip_encoder.mode == "text"
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == 512
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
assert all([isinstance(num, float) for num in embedding])
|
||||
clip_encoder.text_model.run.assert_called_once()
|
||||
|
||||
|
||||
21
machine-learning/export/Dockerfile
Normal file
21
machine-learning/export/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src
|
||||
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER conda-lock.yml /tmp/conda-lock.yml
|
||||
RUN micromamba install -y -n base -f /tmp/conda-lock.yml && \
|
||||
micromamba remove -y -n base cxx-compiler && \
|
||||
micromamba clean --all --yes
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER start.sh .
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER app .
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
|
||||
CMD ["./start.sh"]
|
||||
3520
machine-learning/export/conda-lock.yml
Normal file
3520
machine-learning/export/conda-lock.yml
Normal file
File diff suppressed because it is too large
Load Diff
15
machine-learning/export/env.dev.yaml
Normal file
15
machine-learning/export/env.dev.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
name: base
|
||||
channels:
|
||||
- conda-forge
|
||||
platforms:
|
||||
- linux-64
|
||||
- linux-aarch64
|
||||
dependencies:
|
||||
- black
|
||||
- conda-lock
|
||||
- mypy
|
||||
- pytest
|
||||
- pytest-cov
|
||||
- pytest-mock
|
||||
- ruff
|
||||
category: dev
|
||||
25
machine-learning/export/env.yaml
Normal file
25
machine-learning/export/env.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
name: base
|
||||
channels:
|
||||
- conda-forge
|
||||
- nvidia
|
||||
- pytorch-nightly
|
||||
platforms:
|
||||
- linux-64
|
||||
dependencies:
|
||||
- cxx-compiler
|
||||
- onnx==1.*
|
||||
- onnxruntime==1.*
|
||||
- open-clip-torch==2.*
|
||||
- orjson==3.*
|
||||
- pip
|
||||
- python==3.11.*
|
||||
- pytorch
|
||||
- rich==13.*
|
||||
- safetensors==0.*
|
||||
- setuptools==68.*
|
||||
- torchvision
|
||||
- transformers==4.*
|
||||
- pip:
|
||||
- multilingual-clip
|
||||
- onnx-simplifier
|
||||
category: main
|
||||
0
machine-learning/export/models/__init__.py
Normal file
0
machine-learning/export/models/__init__.py
Normal file
67
machine-learning/export/models/mclip.py
Normal file
67
machine-learning/export/models/mclip.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from .openclip import OpenCLIPModelConfig
|
||||
from .openclip import to_onnx as openclip_to_onnx
|
||||
from .optimize import optimize
|
||||
from .util import get_model_path
|
||||
|
||||
_MCLIP_TO_OPENCLIP = {
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32": OpenCLIPModelConfig("ViT-B-32", "openai"),
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus": OpenCLIPModelConfig("ViT-B-16-plus-240", "laion400m_e32"),
|
||||
"M-CLIP/LABSE-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
||||
}
|
||||
|
||||
|
||||
def to_onnx(
|
||||
model_name: str,
|
||||
output_dir_visual: Path | str,
|
||||
output_dir_textual: Path | str,
|
||||
) -> None:
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = MultilingualCLIP.from_pretrained(model_name, cache_dir=tmpdir)
|
||||
AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual)
|
||||
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
export_text_encoder(model, textual_path)
|
||||
openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
|
||||
optimize(textual_path)
|
||||
|
||||
|
||||
def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def forward(self: MultilingualCLIP, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
|
||||
embs = self.transformer(input_ids, attention_mask)[0]
|
||||
embs = (embs * attention_mask.unsqueeze(2)).sum(dim=1) / attention_mask.sum(dim=1)[:, None]
|
||||
embs = self.LinearTransformation(embs)
|
||||
return torch.nn.functional.normalize(embs, dim=-1)
|
||||
|
||||
# unfortunately need to monkeypatch for tracing to work here
|
||||
# otherwise it hits the 2GiB protobuf serialization limit
|
||||
MultilingualCLIP.forward = forward
|
||||
|
||||
args = (torch.ones(1, 77, dtype=torch.int32), torch.ones(1, 77, dtype=torch.int32))
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
model,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["input_ids", "attention_mask"],
|
||||
output_names=["text_embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={
|
||||
"input_ids": {0: "batch_size", 1: "sequence_length"},
|
||||
"attention_mask": {0: "batch_size", 1: "sequence_length"},
|
||||
},
|
||||
)
|
||||
109
machine-learning/export/models/openclip.py
Normal file
109
machine-learning/export/models/openclip.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import tempfile
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import open_clip
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from .optimize import optimize
|
||||
from .util import get_model_path, save_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenCLIPModelConfig:
|
||||
name: str
|
||||
pretrained: str
|
||||
image_size: int = field(init=False)
|
||||
sequence_length: int = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
open_clip_cfg = open_clip.get_model_config(self.name)
|
||||
if open_clip_cfg is None:
|
||||
raise ValueError(f"Unknown model {self.name}")
|
||||
self.image_size = open_clip_cfg["vision_cfg"]["image_size"]
|
||||
self.sequence_length = open_clip_cfg["text_cfg"]["context_length"]
|
||||
|
||||
|
||||
def to_onnx(
|
||||
model_cfg: OpenCLIPModelConfig,
|
||||
output_dir_visual: Path | str | None = None,
|
||||
output_dir_textual: Path | str | None = None,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = open_clip.create_model(
|
||||
model_cfg.name,
|
||||
pretrained=model_cfg.pretrained,
|
||||
jit=False,
|
||||
cache_dir=tmpdir,
|
||||
require_pretrained=True,
|
||||
)
|
||||
|
||||
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
|
||||
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
if output_dir_visual is not None:
|
||||
output_dir_visual = Path(output_dir_visual)
|
||||
visual_path = get_model_path(output_dir_visual)
|
||||
|
||||
save_config(open_clip.get_model_preprocess_cfg(model), output_dir_visual / "preprocess_cfg.json")
|
||||
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
|
||||
export_image_encoder(model, model_cfg, visual_path)
|
||||
|
||||
optimize(visual_path)
|
||||
|
||||
if output_dir_textual is not None:
|
||||
output_dir_textual = Path(output_dir_textual)
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
|
||||
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
|
||||
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
|
||||
export_text_encoder(model, model_cfg, textual_path)
|
||||
optimize(textual_path)
|
||||
|
||||
|
||||
def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_image(image: torch.Tensor) -> torch.Tensor:
|
||||
return model.encode_image(image, normalize=True)
|
||||
|
||||
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
|
||||
traced = torch.jit.trace(encode_image, args)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
traced,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["image"],
|
||||
output_names=["image_embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={"image": {0: "batch_size"}},
|
||||
)
|
||||
|
||||
|
||||
def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_text(text: torch.Tensor) -> torch.Tensor:
|
||||
return model.encode_text(text, normalize=True)
|
||||
|
||||
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
|
||||
traced = torch.jit.trace(encode_text, args)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
traced,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["text"],
|
||||
output_names=["text_embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={"text": {0: "batch_size"}},
|
||||
)
|
||||
38
machine-learning/export/models/optimize.py
Normal file
38
machine-learning/export/models/optimize.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from pathlib import Path
|
||||
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
import onnxsim
|
||||
|
||||
|
||||
def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
output_path = Path(output_path)
|
||||
model = onnx.load(model_path.as_posix())
|
||||
model, check = onnxsim.simplify(model, skip_shape_inference=True)
|
||||
assert check, "Simplified ONNX model could not be validated"
|
||||
onnx.save(model, output_path.as_posix())
|
||||
|
||||
|
||||
def optimize_ort(
|
||||
model_path: Path | str,
|
||||
output_path: Path | str,
|
||||
level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
|
||||
) -> None:
|
||||
model_path = Path(model_path)
|
||||
output_path = Path(output_path)
|
||||
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.graph_optimization_level = level
|
||||
sess_options.optimized_model_filepath = output_path.as_posix()
|
||||
|
||||
ort.InferenceSession(model_path.as_posix(), providers=["CPUExecutionProvider"], sess_options=sess_options)
|
||||
|
||||
|
||||
def optimize(model_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
|
||||
optimize_ort(model_path, model_path)
|
||||
# onnxsim serializes large models as a blob, which uses much more memory when loading the model at runtime
|
||||
if not any(file.name.startswith("Constant") for file in model_path.parent.iterdir()):
|
||||
optimize_onnxsim(model_path, model_path)
|
||||
15
machine-learning/export/models/util.py
Normal file
15
machine-learning/export/models/util.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_model_path(output_dir: Path | str) -> Path:
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
return output_dir / "model.onnx"
|
||||
|
||||
|
||||
def save_config(config: Any, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
json.dump(config, output_path.open("w"))
|
||||
76
machine-learning/export/run.py
Normal file
76
machine-learning/export/run.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import gc
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from huggingface_hub import create_repo, login, upload_folder
|
||||
from models import mclip, openclip
|
||||
from rich.progress import Progress
|
||||
|
||||
models = [
|
||||
"RN50::openai",
|
||||
"RN50::yfcc15m",
|
||||
"RN50::cc12m",
|
||||
"RN101::openai",
|
||||
"RN101::yfcc15m",
|
||||
"RN50x4::openai",
|
||||
"RN50x16::openai",
|
||||
"RN50x64::openai",
|
||||
"ViT-B-32::openai",
|
||||
"ViT-B-32::laion2b_e16",
|
||||
"ViT-B-32::laion400m_e31",
|
||||
"ViT-B-32::laion400m_e32",
|
||||
"ViT-B-32::laion2b-s34b-b79k",
|
||||
"ViT-B-16::openai",
|
||||
"ViT-B-16::laion400m_e31",
|
||||
"ViT-B-16::laion400m_e32",
|
||||
"ViT-B-16-plus-240::laion400m_e31",
|
||||
"ViT-B-16-plus-240::laion400m_e32",
|
||||
"ViT-L-14::openai",
|
||||
"ViT-L-14::laion400m_e31",
|
||||
"ViT-L-14::laion400m_e32",
|
||||
"ViT-L-14::laion2b-s32b-b82k",
|
||||
"ViT-L-14-336::openai",
|
||||
"ViT-H-14::laion2b-s32b-b79k",
|
||||
"ViT-g-14::laion2b-s12b-b42k",
|
||||
"M-CLIP/LABSE-Vit-L-14",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
||||
]
|
||||
|
||||
login(token=os.environ["HF_AUTH_TOKEN"])
|
||||
|
||||
with Progress() as progress:
|
||||
task1 = progress.add_task("[green]Exporting models...", total=len(models))
|
||||
task2 = progress.add_task("[yellow]Uploading models...", total=len(models))
|
||||
|
||||
with TemporaryDirectory() as tmp:
|
||||
tmpdir = Path(tmp)
|
||||
for model in models:
|
||||
model_name = model.split("/")[-1].replace("::", "__")
|
||||
config_path = tmpdir / model_name / "config.json"
|
||||
|
||||
def upload() -> None:
|
||||
progress.update(task2, description=f"[yellow]Uploading {model_name}")
|
||||
repo_id = f"immich-app/{model_name}"
|
||||
|
||||
create_repo(repo_id, exist_ok=True)
|
||||
upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name)
|
||||
progress.update(task2, advance=1)
|
||||
|
||||
def export() -> None:
|
||||
progress.update(task1, description=f"[green]Exporting {model_name}")
|
||||
visual_dir = tmpdir / model_name / "visual"
|
||||
textual_dir = tmpdir / model_name / "textual"
|
||||
if model.startswith("M-CLIP"):
|
||||
mclip.to_onnx(model, visual_dir, textual_dir)
|
||||
else:
|
||||
name, _, pretrained = model_name.partition("__")
|
||||
openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir)
|
||||
|
||||
progress.update(task1, advance=1)
|
||||
gc.collect()
|
||||
|
||||
export()
|
||||
upload()
|
||||
@@ -1,11 +1,12 @@
|
||||
from io import BytesIO
|
||||
import json
|
||||
from argparse import ArgumentParser
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
from locust import HttpUser, events, task
|
||||
from locust.env import Environment
|
||||
from PIL import Image
|
||||
from argparse import ArgumentParser
|
||||
|
||||
byte_image = BytesIO()
|
||||
|
||||
|
||||
@@ -14,11 +15,21 @@ def _(parser: ArgumentParser) -> None:
|
||||
parser.add_argument("--tag-model", type=str, default="microsoft/resnet-50")
|
||||
parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai")
|
||||
parser.add_argument("--face-model", type=str, default="buffalo_l")
|
||||
parser.add_argument("--tag-min-score", type=int, default=0.0,
|
||||
help="Returns all tags at or above this score. The default returns all tags.")
|
||||
parser.add_argument("--face-min-score", type=int, default=0.034,
|
||||
help=("Returns all faces at or above this score. The default returns 1 face per request; "
|
||||
"setting this to 0 blows up the number of faces to the thousands."))
|
||||
parser.add_argument(
|
||||
"--tag-min-score",
|
||||
type=int,
|
||||
default=0.0,
|
||||
help="Returns all tags at or above this score. The default returns all tags.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--face-min-score",
|
||||
type=int,
|
||||
default=0.034,
|
||||
help=(
|
||||
"Returns all faces at or above this score. The default returns 1 face per request; "
|
||||
"setting this to 0 blows up the number of faces to the thousands."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--image-size", type=int, default=1000)
|
||||
|
||||
|
||||
@@ -62,7 +73,7 @@ class CLIPTextFormDataLoadTest(InferenceLoadTest):
|
||||
("modelName", self.environment.parsed_options.clip_model),
|
||||
("modelType", "clip"),
|
||||
("options", json.dumps({"mode": "text"})),
|
||||
("text", "test search query")
|
||||
("text", "test search query"),
|
||||
]
|
||||
self.client.post("/predict", data=data)
|
||||
|
||||
@@ -88,5 +99,5 @@ class RecognitionFormDataLoadTest(InferenceLoadTest):
|
||||
("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})),
|
||||
]
|
||||
files = {"image": self.data}
|
||||
|
||||
|
||||
self.client.post("/predict", data=data, files=files)
|
||||
|
||||
3875
machine-learning/poetry.lock
generated
3875
machine-learning/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.83.0"
|
||||
version = "1.84.0"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -9,8 +9,8 @@ packages = [{include = "app"}]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
torch = [
|
||||
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.0.1", source = "pypi"},
|
||||
{markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.0.1", source = "pytorch-cpu"}
|
||||
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.1.0", source = "pypi"},
|
||||
{markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.1.0", source = "pytorch-cpu"}
|
||||
]
|
||||
transformers = "^4.29.2"
|
||||
onnxruntime = "^1.15.0"
|
||||
@@ -22,14 +22,9 @@ uvicorn = {extras = ["standard"], version = "^0.22.0"}
|
||||
pydantic = "^1.10.8"
|
||||
aiocache = "^0.12.1"
|
||||
optimum = "^1.9.1"
|
||||
torchvision = [
|
||||
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=0.15.2", source = "pypi"},
|
||||
{markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=0.15.2", source = "pytorch-cpu"}
|
||||
]
|
||||
rich = "^13.4.2"
|
||||
ftfy = "^6.1.1"
|
||||
setuptools = "^68.0.0"
|
||||
open-clip-torch = "^2.20.0"
|
||||
python-multipart = "^0.0.6"
|
||||
orjson = "^3.9.5"
|
||||
safetensors = "0.3.2"
|
||||
@@ -63,6 +58,7 @@ warn_redundant_casts = true
|
||||
disallow_any_generics = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pydantic-mypy]
|
||||
init_forbid_extra = true
|
||||
@@ -70,30 +66,6 @@ init_typed = true
|
||||
warn_required_dynamic_aliases = true
|
||||
warn_untyped_fields = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"huggingface_hub",
|
||||
"transformers",
|
||||
"gunicorn",
|
||||
"cv2",
|
||||
"insightface.model_zoo",
|
||||
"insightface.utils.face_align",
|
||||
"insightface.utils.storage",
|
||||
"onnxruntime",
|
||||
"optimum",
|
||||
"optimum.pipelines",
|
||||
"optimum.onnxruntime",
|
||||
"clip_server.model.clip",
|
||||
"clip_server.model.clip_onnx",
|
||||
"clip_server.model.pretrained_models",
|
||||
"clip_server.model.tokenization",
|
||||
"torchvision.transforms",
|
||||
"aiocache.backends.memory",
|
||||
"aiocache.lock",
|
||||
"aiocache.plugins"
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# requirements to be installed with `--no-deps` flag
|
||||
clip-server==0.8.*
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 107,
|
||||
"android.injected.version.name" => "1.83.0",
|
||||
"android.injected.version.code" => 108,
|
||||
"android.injected.version.name" => "1.84.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
* User can now download assets to local device
|
||||
* Increased the font size for curated image thumbnail information on the seach page
|
||||
* Increased the font size for curated image thumbnail information on the search page
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.160108">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.176668">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -253,6 +253,8 @@
|
||||
"profile_drawer_settings": "Settings",
|
||||
"profile_drawer_sign_out": "Sign Out",
|
||||
"profile_drawer_trash": "Trash",
|
||||
"profile_drawer_documentation": "Documentation",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"recently_added_page_title": "Recently Added",
|
||||
"search_bar_hint": "Search your photos",
|
||||
"search_page_categories": "Categories",
|
||||
@@ -277,6 +279,7 @@
|
||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||
"server_info_box_app_version": "App Version",
|
||||
"server_info_box_server_version": "Server Version",
|
||||
"server_info_box_server_url": "Server URL",
|
||||
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
||||
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
||||
"setting_image_viewer_original_title": "Load original image",
|
||||
@@ -311,6 +314,8 @@
|
||||
"shared_link_edit_change_expiry": "Change expiration time",
|
||||
"shared_link_edit_description": "Description",
|
||||
"shared_link_edit_description_hint": "Enter the share description",
|
||||
"shared_link_edit_password": "Password",
|
||||
"shared_link_edit_password_hint": "Enter the share password",
|
||||
"shared_link_edit_show_meta": "Show metadata",
|
||||
"shared_link_edit_submit_button": "Update link",
|
||||
"shared_link_empty": "You don't have any shared links",
|
||||
@@ -364,5 +369,8 @@
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"cache_settings_tile_title": "Local Storage",
|
||||
"cache_settings_tile_subtitle": "Control the local storage behaviour",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset"
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"app_bar_signout_dialog_title": "Sign out",
|
||||
"app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
|
||||
"app_bar_signout_dialog_ok": "Yes"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ PODS:
|
||||
- Flutter
|
||||
- isar_flutter_libs (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -42,6 +48,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -55,6 +63,8 @@ PODS:
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
@@ -71,16 +81,21 @@ DEPENDENCIES:
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -115,6 +130,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
isar_flutter_libs:
|
||||
:path: ".symlinks/plugins/isar_flutter_libs/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
@@ -125,6 +146,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
photo_manager:
|
||||
:path: ".symlinks/plugins/photo_manager/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -135,6 +158,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
@@ -152,6 +177,9 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
@@ -159,14 +187,16 @@ SPEC CHECKSUMS:
|
||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 118;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -515,7 +515,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 118;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 118;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.78.1</string>
|
||||
<string>1.84.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>118</string>
|
||||
<string>124</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.83.0"
|
||||
version_number: "1.84.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000256">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="7.645306">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.669798">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.218788">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="97.596654">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="145.399278">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="89.490906">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void main() async {
|
||||
@@ -49,6 +50,7 @@ void main() async {
|
||||
|
||||
Future<void> initApp() async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
if (kReleaseMode && Platform.isAndroid) {
|
||||
try {
|
||||
|
||||
@@ -265,6 +265,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
if (data.isRemote) buildControlButton(data),
|
||||
],
|
||||
),
|
||||
isOwner: userId == data.ownerId,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,12 +10,16 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.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_app_bar.dart';
|
||||
|
||||
class LibraryPage extends HookConsumerWidget {
|
||||
const LibraryPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider);
|
||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
var settings = ref.watch(appSettingsServiceProvider);
|
||||
@@ -28,21 +32,6 @@ class LibraryPage extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
AppBar buildAppBar() {
|
||||
return AppBar(
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final selectedAlbumSortOrder =
|
||||
useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
|
||||
|
||||
@@ -236,8 +225,23 @@ class LibraryPage extends HookConsumerWidget {
|
||||
|
||||
final local = albums.where((a) => a.isLocal).toList();
|
||||
|
||||
Widget? shareTrashButton() {
|
||||
return trashEnabled
|
||||
? InkWell(
|
||||
onTap: () => AutoRouter.of(context).push(const TrashRoute()),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Icon(
|
||||
Icons.delete_rounded,
|
||||
size: 25,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: buildAppBar(),
|
||||
appBar: ImmichAppBar(
|
||||
action: shareTrashButton(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class SharingPage extends HookConsumerWidget {
|
||||
@@ -167,32 +168,6 @@ class SharingPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
AppBar buildAppBar() {
|
||||
return AppBar(
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
iconSize: 20,
|
||||
icon: const Icon(
|
||||
Icons.swap_horizontal_circle_outlined,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => AutoRouter.of(context).push(const PartnerRoute()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
buildEmptyListIndication() {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -241,8 +216,21 @@ class SharingPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget sharePartnerButton() {
|
||||
return InkWell(
|
||||
onTap: () => AutoRouter.of(context).push(const PartnerRoute()),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Icon(
|
||||
Icons.swap_horizontal_circle_rounded,
|
||||
size: 25,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: buildAppBar(),
|
||||
appBar: ImmichAppBar(
|
||||
action: sharePartnerButton(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: buildTopBottons()),
|
||||
|
||||
@@ -31,7 +31,14 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
final owner = ref.watch(currentUserProvider);
|
||||
final hasError = useState(false);
|
||||
|
||||
controller.text = description;
|
||||
useEffect(
|
||||
() {
|
||||
controller.text = description;
|
||||
isTextEmpty.value = description.isEmpty;
|
||||
return null;
|
||||
},
|
||||
[description],
|
||||
);
|
||||
|
||||
submitDescription(String description) async {
|
||||
hasError.value = false;
|
||||
|
||||
@@ -297,9 +297,9 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text(
|
||||
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
|
||||
),
|
||||
) : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
required this.isPlayingMotionVideo,
|
||||
required this.onFavorite,
|
||||
required this.onUploadPressed,
|
||||
required this.isOwner,
|
||||
}) : super(key: key);
|
||||
|
||||
final Asset asset;
|
||||
@@ -25,6 +26,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
final VoidCallback onAddToAlbumPressed;
|
||||
final Function(Asset) onFavorite;
|
||||
final bool isPlayingMotionVideo;
|
||||
final bool isOwner;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -123,11 +125,11 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
size: iconSize,
|
||||
),
|
||||
actions: [
|
||||
if (asset.isRemote) buildFavoriteButton(a),
|
||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
|
||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||
if (asset.isRemote && !asset.isLocal) buildDownloadButton(),
|
||||
if (asset.isRemote) buildAddToAlbumButtom(),
|
||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
|
||||
buildMoreInfoButton(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final int initialIndex;
|
||||
final int heroOffset;
|
||||
final bool showStack;
|
||||
final bool isOwner;
|
||||
|
||||
GalleryViewerPage({
|
||||
super.key,
|
||||
@@ -56,6 +57,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
required this.totalAssets,
|
||||
this.heroOffset = 0,
|
||||
this.showStack = false,
|
||||
this.isOwner = true,
|
||||
}) : controller = PageController(initialPage: initialIndex);
|
||||
|
||||
final PageController controller;
|
||||
@@ -88,7 +90,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
: <Asset>[];
|
||||
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromResponse = currentAsset.id == Isar.autoIncrement;
|
||||
final isFromDto = currentAsset.id == Isar.autoIncrement;
|
||||
|
||||
Asset asset() => stackIndex.value == -1
|
||||
? currentAsset
|
||||
@@ -334,6 +336,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: TopControlAppBar(
|
||||
isOwner: isOwner,
|
||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||
asset: asset(),
|
||||
onMoreInfoPressed: showInfo,
|
||||
@@ -573,35 +576,50 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
label: 'control_bottom_app_bar_share'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_share'.tr(),
|
||||
),
|
||||
asset().isArchived
|
||||
? BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.unarchive_rounded),
|
||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
)
|
||||
: BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.archive_outlined),
|
||||
label: 'control_bottom_app_bar_archive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||
),
|
||||
if (stack.isNotEmpty)
|
||||
if (isOwner)
|
||||
asset().isArchived
|
||||
? BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.unarchive_rounded),
|
||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
)
|
||||
: BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.archive_outlined),
|
||||
label: 'control_bottom_app_bar_archive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||
),
|
||||
if (isOwner && stack.isNotEmpty)
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.burst_mode_outlined),
|
||||
label: 'control_bottom_app_bar_stack'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_stack'.tr(),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: 'control_bottom_app_bar_delete'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||
),
|
||||
if (isOwner)
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: 'control_bottom_app_bar_delete'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||
),
|
||||
if (!isOwner)
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.download_outlined),
|
||||
label: 'download'.tr(),
|
||||
tooltip: 'download'.tr(),
|
||||
),
|
||||
];
|
||||
|
||||
List<Function(int)> actionslist = [
|
||||
(_) => shareAsset(),
|
||||
(_) => handleArchive(asset()),
|
||||
if (stack.isNotEmpty) (_) => showStackActionItems(),
|
||||
(_) => handleDelete(asset()),
|
||||
if (isOwner) (_) => handleArchive(asset()),
|
||||
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
|
||||
if (isOwner) (_) => handleDelete(asset()),
|
||||
if (!isOwner)
|
||||
(_) => asset().isLocal
|
||||
? null
|
||||
: ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
asset(),
|
||||
context,
|
||||
),
|
||||
];
|
||||
|
||||
return IgnorePointer(
|
||||
@@ -755,7 +773,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
},
|
||||
imageProvider: provider,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: isFromResponse
|
||||
tag: isFromDto
|
||||
? '${a.remoteId}-$heroOffset'
|
||||
: a.id + heroOffset,
|
||||
),
|
||||
@@ -773,11 +791,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
localPosition = details.localPosition,
|
||||
onDragUpdate: (_, details, __) =>
|
||||
handleSwipeUpDown(details),
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: isFromResponse
|
||||
? '${a.remoteId}-$heroOffset'
|
||||
: a.id + heroOffset,
|
||||
),
|
||||
filterQuality: FilterQuality.high,
|
||||
maxScale: 1.0,
|
||||
minScale: 1.0,
|
||||
|
||||
@@ -9,6 +9,8 @@ import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -125,99 +127,128 @@ class VideoPlayer extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VideoPlayerState extends State<VideoPlayer> {
|
||||
late VideoPlayerController videoPlayerController;
|
||||
ChewieController? chewieController;
|
||||
// late VideoPlayerController videoPlayerController;
|
||||
// ChewieController? chewieController;
|
||||
|
||||
// Create a [Player] to control playback.
|
||||
late final player = Player();
|
||||
// Create a [VideoController] to handle video output from [Player].
|
||||
late final controller = VideoController(player);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializePlayer();
|
||||
// initializePlayer();
|
||||
|
||||
videoPlayerController.addListener(() {
|
||||
if (videoPlayerController.value.isInitialized) {
|
||||
if (videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.enable();
|
||||
widget.onPlaying?.call();
|
||||
} else if (!videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.disable();
|
||||
widget.onPaused?.call();
|
||||
}
|
||||
// videoPlayerController.addListener(() {
|
||||
// if (videoPlayerController.value.isInitialized) {
|
||||
// if (videoPlayerController.value.isPlaying) {
|
||||
// WakelockPlus.enable();
|
||||
// widget.onPlaying?.call();
|
||||
// } else if (!videoPlayerController.value.isPlaying) {
|
||||
// WakelockPlus.disable();
|
||||
// widget.onPaused?.call();
|
||||
// }
|
||||
|
||||
if (videoPlayerController.value.position ==
|
||||
videoPlayerController.value.duration) {
|
||||
WakelockPlus.disable();
|
||||
widget.onVideoEnded();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// if (videoPlayerController.value.position ==
|
||||
// videoPlayerController.value.duration) {
|
||||
// WakelockPlus.disable();
|
||||
// widget.onVideoEnded();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
try {
|
||||
videoPlayerController = widget.file == null
|
||||
? VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.url!),
|
||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||
)
|
||||
: VideoPlayerController.file(widget.file!);
|
||||
|
||||
await videoPlayerController.initialize();
|
||||
_createChewieController();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
debugPrint("ERROR initialize video player $e");
|
||||
if (widget.file == null) {
|
||||
player.open(
|
||||
Media(
|
||||
Uri.parse(widget.url!).toString(),
|
||||
httpHeaders: {
|
||||
"Authorization": "Bearer ${widget.jwtToken}",
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
player.open(
|
||||
Media(
|
||||
widget.file!.path,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_createChewieController() {
|
||||
chewieController = ChewieController(
|
||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
showOptions: true,
|
||||
showControlsOnInitialize: false,
|
||||
videoPlayerController: videoPlayerController,
|
||||
autoPlay: true,
|
||||
autoInitialize: true,
|
||||
allowFullScreen: false,
|
||||
allowedScreenSleep: false,
|
||||
showControls: !widget.isMotionVideo,
|
||||
customControls: const VideoPlayerControls(),
|
||||
hideControlsTimer: const Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
// Future<void> initializePlayer() async {
|
||||
// try {
|
||||
// videoPlayerController = widget.file == null
|
||||
// ? VideoPlayerController.networkUrl(
|
||||
// Uri.parse(widget.url!),
|
||||
// httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||
// )
|
||||
// : VideoPlayerController.file(widget.file!);
|
||||
|
||||
// await videoPlayerController.initialize();
|
||||
// _createChewieController();
|
||||
// setState(() {});
|
||||
// } catch (e) {
|
||||
// debugPrint("ERROR initialize video player $e");
|
||||
// }
|
||||
// }
|
||||
|
||||
// _createChewieController() {
|
||||
// chewieController = ChewieController(
|
||||
// controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
// bottom: 100,
|
||||
// ),
|
||||
// showOptions: true,
|
||||
// showControlsOnInitialize: false,
|
||||
// videoPlayerController: videoPlayerController,
|
||||
// autoPlay: true,
|
||||
// autoInitialize: true,
|
||||
// allowFullScreen: false,
|
||||
// allowedScreenSleep: false,
|
||||
// showControls: !widget.isMotionVideo,
|
||||
// customControls: const VideoPlayerControls(),
|
||||
// hideControlsTimer: const Duration(seconds: 5),
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
videoPlayerController.pause();
|
||||
videoPlayerController.dispose();
|
||||
chewieController?.dispose();
|
||||
player.dispose();
|
||||
// videoPlayerController.pause();
|
||||
// videoPlayerController.dispose();
|
||||
// chewieController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (chewieController?.videoPlayerController.value.isInitialized == true) {
|
||||
return SizedBox(
|
||||
child: Chewie(
|
||||
controller: chewieController!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.placeholder != null) widget.placeholder!,
|
||||
const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Video(controller: controller),
|
||||
);
|
||||
|
||||
// if (chewieController?.videoPlayerController.value.isInitialized == true) {
|
||||
// return SizedBox(
|
||||
// child: Chewie(
|
||||
// controller: chewieController!,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// return SizedBox(
|
||||
// height: MediaQuery.of(context).size.height,
|
||||
// width: MediaQuery.of(context).size.width,
|
||||
// child: Center(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// if (widget.placeholder != null) widget.placeholder!,
|
||||
// const Center(
|
||||
// child: ImmichLoadingIndicator(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,46 +174,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStorageInformation() {
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
Icons.storage_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
title: const Text(
|
||||
"backup_controller_page_server_storage",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
isThreeLine: true,
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||
backgroundColor: Colors.grey,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: const Text('backup_controller_page_storage_format').tr(
|
||||
args: [
|
||||
backupState.serverInfo.diskUse,
|
||||
backupState.serverInfo.diskSize,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ListTile buildAutoBackupController() {
|
||||
final isAutoBackup = backupState.autoBackup;
|
||||
final backUpOption = isAutoBackup
|
||||
@@ -774,7 +734,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
if (showBackupFix) const Divider(),
|
||||
if (showBackupFix) buildCheckCorruptBackups(),
|
||||
const Divider(),
|
||||
buildStorageInformation(),
|
||||
const Divider(),
|
||||
const CurrentUploadingAssetInfoBox(),
|
||||
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
|
||||
|
||||
@@ -33,6 +33,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||
final bool shrinkWrap;
|
||||
final bool showDragScroll;
|
||||
final bool showStack;
|
||||
final bool isOwner;
|
||||
|
||||
const ImmichAssetGrid({
|
||||
super.key,
|
||||
@@ -53,6 +54,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||
this.shrinkWrap = false,
|
||||
this.showDragScroll = true,
|
||||
this.showStack = false,
|
||||
this.isOwner = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -117,6 +119,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||
shrinkWrap: shrinkWrap,
|
||||
showDragScroll: showDragScroll,
|
||||
showStack: showStack,
|
||||
isOwner: isOwner,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||
final bool shrinkWrap;
|
||||
final bool showDragScroll;
|
||||
final bool showStack;
|
||||
final bool isOwner;
|
||||
|
||||
const ImmichAssetGridView({
|
||||
super.key,
|
||||
@@ -58,6 +59,7 @@ class ImmichAssetGridView extends StatefulWidget {
|
||||
this.shrinkWrap = false,
|
||||
this.showDragScroll = true,
|
||||
this.showStack = false,
|
||||
this.isOwner = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -138,6 +140,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
showStorageIndicator: widget.showStorageIndicator,
|
||||
heroOffset: widget.heroOffset,
|
||||
showStack: widget.showStack,
|
||||
isOwner: widget.isOwner,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
final int totalAssets;
|
||||
final bool showStorageIndicator;
|
||||
final bool showStack;
|
||||
final bool isOwner;
|
||||
final bool useGrayBoxPlaceholder;
|
||||
final bool isSelected;
|
||||
final bool multiselectEnabled;
|
||||
@@ -29,6 +30,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
required this.totalAssets,
|
||||
this.showStorageIndicator = true,
|
||||
this.showStack = false,
|
||||
this.isOwner = true,
|
||||
this.useGrayBoxPlaceholder = false,
|
||||
this.isSelected = false,
|
||||
this.multiselectEnabled = false,
|
||||
@@ -43,7 +45,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
final assetContainerColor =
|
||||
isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight;
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromResponse = asset.id == Isar.autoIncrement;
|
||||
final isFromDto = asset.id == Isar.autoIncrement;
|
||||
|
||||
Widget buildSelectionIcon(Asset asset) {
|
||||
if (isSelected) {
|
||||
@@ -132,7 +134,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: Hero(
|
||||
tag: isFromResponse
|
||||
tag: isFromDto
|
||||
? '${asset.remoteId}-$heroOffset'
|
||||
: asset.id + heroOffset,
|
||||
child: ImmichImage(
|
||||
@@ -181,6 +183,7 @@ class ThumbnailImage extends StatelessWidget {
|
||||
totalAssets: totalAssets,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
isOwner: isOwner,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
|
||||
class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
|
||||
const HomePageAppBar({
|
||||
super.key,
|
||||
this.onPopBack,
|
||||
});
|
||||
|
||||
final Function? onPopBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final BackUpState backupState = ref.watch(backupProvider);
|
||||
final bool isEnableAutoBackup =
|
||||
backupState.backgroundBackup || backupState.autoBackup;
|
||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
buildProfilePhoto() {
|
||||
if (authState.profileImagePath.isEmpty || user == null) {
|
||||
return IconButton(
|
||||
splashRadius: 25,
|
||||
icon: const Icon(
|
||||
Icons.face_outlined,
|
||||
size: 30,
|
||||
),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
child: UserCircleAvatar(
|
||||
radius: 18,
|
||||
size: 33,
|
||||
user: user,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
leading: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: buildProfilePhoto(),
|
||||
),
|
||||
if (serverInfoState.isVersionMismatch)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () => Scaffold.of(context).openDrawer(),
|
||||
child: Material(
|
||||
// color: Colors.grey[200],
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(2.0),
|
||||
child: Icon(
|
||||
Icons.info,
|
||||
color: Color.fromARGB(255, 243, 188, 106),
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 12,
|
||||
child: SizedBox(
|
||||
height: 8,
|
||||
width: 8,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
iconSize: 30,
|
||||
icon: isEnableAutoBackup
|
||||
? const Icon(
|
||||
Icons.backup_rounded,
|
||||
)
|
||||
: Badge(
|
||||
padding: const EdgeInsets.all(4),
|
||||
backgroundColor: Colors.white,
|
||||
label: const Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 8,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.backup_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
var onPop = await AutoRouter.of(context)
|
||||
.push(const BackupControllerRoute());
|
||||
|
||||
if (onPop != null && onPop == true) {
|
||||
onPopBack!();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
child: Text(
|
||||
'${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}',
|
||||
style:
|
||||
const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
|
||||
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 {
|
||||
const ProfileDrawer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
|
||||
buildSignOutButton() {
|
||||
return ListTile(
|
||||
leading: SizedBox(
|
||||
height: double.infinity,
|
||||
child: Icon(
|
||||
Icons.logout_rounded,
|
||||
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"profile_drawer_sign_out",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
onTap: () async {
|
||||
await ref.watch(authenticationProvider.notifier).logout();
|
||||
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.watch(backupProvider.notifier).cancelBackup();
|
||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
AutoRouter.of(context).replace(const LoginRoute());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildSettingButton() {
|
||||
return ListTile(
|
||||
leading: SizedBox(
|
||||
height: double.infinity,
|
||||
child: Icon(
|
||||
Icons.settings_rounded,
|
||||
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"profile_drawer_settings",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(const SettingsRoute());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildAppLogButton() {
|
||||
return ListTile(
|
||||
leading: SizedBox(
|
||||
height: double.infinity,
|
||||
child: Icon(
|
||||
Icons.assignment_outlined,
|
||||
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"profile_drawer_app_logs",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(const AppLogRoute());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildTrashButton() {
|
||||
return ListTile(
|
||||
leading: SizedBox(
|
||||
height: double.infinity,
|
||||
child: Icon(
|
||||
Icons.delete_rounded,
|
||||
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"profile_drawer_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,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const ProfileDrawerHeader(),
|
||||
buildSettingButton(),
|
||||
buildAppLogButton(),
|
||||
if (trashEnabled) buildTrashButton(),
|
||||
buildSignOutButton(),
|
||||
],
|
||||
),
|
||||
const ServerInfoBox(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class ServerInfoBox extends HookConsumerWidget {
|
||||
const ServerInfoBox({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
|
||||
final appInfo = useState({});
|
||||
|
||||
getPackageInfo() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
appInfo.value = {
|
||||
"version": packageInfo.version,
|
||||
"buildNumber": packageInfo.buildNumber,
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
getPackageInfo();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5), // if you need this
|
||||
side: const BorderSide(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
serverInfoState.isVersionMismatch
|
||||
? serverInfoState.versionMismatchErrorMessage
|
||||
: "profile_drawer_client_server_up_to_date".tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
thickness: 1,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"server_info_box_app_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
thickness: 1,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"server_info_box_server_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
serverInfoState.serverVersion.major > 0
|
||||
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
|
||||
: "?",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,7 @@ import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_lane.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
@@ -27,6 +25,7 @@ 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/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||
@@ -74,10 +73,6 @@ class HomePage extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
void reloadAllAsset() {
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
void selectionListener(
|
||||
bool multiselect,
|
||||
@@ -375,10 +370,7 @@ class HomePage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: !selectionEnabledHook.value
|
||||
? HomePageAppBar(onPopBack: reloadAllAsset)
|
||||
: null,
|
||||
drawer: const ProfileDrawer(),
|
||||
appBar: !selectionEnabledHook.value ? const ImmichAppBar() : null,
|
||||
body: buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ class MemoryLane extends HookConsumerWidget {
|
||||
final memoryLane = memoryLaneFutureProvider
|
||||
.whenData(
|
||||
(memories) => memories != null
|
||||
? SizedBox(
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
||||
@@ -9,6 +9,7 @@ class SharedLink {
|
||||
final bool allowUpload;
|
||||
final String? thumbAssetId;
|
||||
final String? description;
|
||||
final String? password;
|
||||
final DateTime? expiresAt;
|
||||
final String key;
|
||||
final bool showMetadata;
|
||||
@@ -21,6 +22,7 @@ class SharedLink {
|
||||
required this.allowUpload,
|
||||
required this.thumbAssetId,
|
||||
required this.description,
|
||||
required this.password,
|
||||
required this.expiresAt,
|
||||
required this.key,
|
||||
required this.showMetadata,
|
||||
@@ -34,6 +36,7 @@ class SharedLink {
|
||||
bool? allowDownload,
|
||||
bool? allowUpload,
|
||||
String? description,
|
||||
String? password,
|
||||
DateTime? expiresAt,
|
||||
String? key,
|
||||
bool? showMetadata,
|
||||
@@ -46,6 +49,7 @@ class SharedLink {
|
||||
allowDownload: allowDownload ?? this.allowDownload,
|
||||
allowUpload: allowUpload ?? this.allowUpload,
|
||||
description: description ?? this.description,
|
||||
password: password ?? this.password,
|
||||
expiresAt: expiresAt ?? this.expiresAt,
|
||||
key: key ?? this.key,
|
||||
showMetadata: showMetadata ?? this.showMetadata,
|
||||
@@ -58,6 +62,7 @@ class SharedLink {
|
||||
allowDownload = dto.allowDownload,
|
||||
allowUpload = dto.allowUpload,
|
||||
description = dto.description,
|
||||
password = dto.password,
|
||||
expiresAt = dto.expiresAt,
|
||||
key = dto.key,
|
||||
showMetadata = dto.showMetadata,
|
||||
@@ -75,7 +80,7 @@ class SharedLink {
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -87,6 +92,7 @@ class SharedLink {
|
||||
other.allowDownload == allowDownload &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.description == description &&
|
||||
other.password == password &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.key == key &&
|
||||
other.showMetadata == showMetadata &&
|
||||
@@ -100,6 +106,7 @@ class SharedLink {
|
||||
allowDownload.hashCode ^
|
||||
allowUpload.hashCode ^
|
||||
description.hashCode ^
|
||||
password.hashCode ^
|
||||
expiresAt.hashCode ^
|
||||
key.hashCode ^
|
||||
showMetadata.hashCode ^
|
||||
|
||||
@@ -40,6 +40,7 @@ class SharedLinkService {
|
||||
required bool allowDownload,
|
||||
required bool allowUpload,
|
||||
String? description,
|
||||
String? password,
|
||||
String? albumId,
|
||||
List<String>? assetIds,
|
||||
DateTime? expiresAt,
|
||||
@@ -57,6 +58,7 @@ class SharedLinkService {
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
password: password,
|
||||
);
|
||||
} else if (assetIds != null) {
|
||||
dto = SharedLinkCreateDto(
|
||||
@@ -66,6 +68,7 @@ class SharedLinkService {
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
password: password,
|
||||
assetIds: assetIds,
|
||||
);
|
||||
}
|
||||
@@ -90,6 +93,7 @@ class SharedLinkService {
|
||||
required bool? allowUpload,
|
||||
bool? changeExpiry = false,
|
||||
String? description,
|
||||
String? password,
|
||||
DateTime? expiresAt,
|
||||
}) async {
|
||||
try {
|
||||
@@ -101,6 +105,7 @@ class SharedLinkService {
|
||||
allowUpload: allowUpload,
|
||||
expiresAt: expiresAt,
|
||||
description: description,
|
||||
password: password,
|
||||
changeExpiryTime: changeExpiry,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -30,6 +30,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
final descriptionController =
|
||||
useTextEditingController(text: existingLink?.description ?? "");
|
||||
final descriptionFocusNode = useFocusNode();
|
||||
final passwordController =
|
||||
useTextEditingController(text: existingLink?.password ?? "");
|
||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||
@@ -113,6 +115,31 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPasswordField() {
|
||||
return TextField(
|
||||
controller: passwordController,
|
||||
enabled: newShareLink.value.isEmpty,
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'shared_link_edit_password'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeData.primaryColor,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'shared_link_edit_password_hint'.tr(),
|
||||
hintStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildShowMetaButton() {
|
||||
return SwitchListTile.adaptive(
|
||||
value: showMetadata.value,
|
||||
@@ -229,7 +256,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
void copyLinkToClipboard() {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: newShareLink.value,
|
||||
text: passwordController.text.isEmpty
|
||||
? newShareLink.value
|
||||
: "Link: ${newShareLink.value}\nPassword: ${passwordController.text}",
|
||||
),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -302,6 +331,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
description: descriptionController.text.isEmpty
|
||||
? null
|
||||
: descriptionController.text,
|
||||
password: passwordController.text.isEmpty
|
||||
? null
|
||||
: passwordController.text,
|
||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
@@ -324,6 +356,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
bool? upload;
|
||||
bool? meta;
|
||||
String? desc;
|
||||
String? password;
|
||||
DateTime? expiry;
|
||||
bool? changeExpiry;
|
||||
|
||||
@@ -343,6 +376,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
desc = descriptionController.text;
|
||||
}
|
||||
|
||||
if (passwordController.text != existingLink!.password) {
|
||||
password = passwordController.text;
|
||||
}
|
||||
|
||||
if (editExpiry.value) {
|
||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||
changeExpiry = true;
|
||||
@@ -354,6 +391,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
allowDownload: download,
|
||||
allowUpload: upload,
|
||||
description: desc,
|
||||
password: password,
|
||||
expiresAt: expiry,
|
||||
changeExpiry: changeExpiry,
|
||||
);
|
||||
@@ -385,6 +423,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(padding),
|
||||
child: buildDescriptionField(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(padding),
|
||||
child: buildPasswordField(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: padding,
|
||||
|
||||
@@ -133,10 +133,7 @@ part 'router.gr.dart';
|
||||
DuplicateGuard,
|
||||
],
|
||||
),
|
||||
CustomRoute(
|
||||
page: AppLogPage,
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
),
|
||||
AutoRoute(page: AppLogPage, guards: [DuplicateGuard]),
|
||||
AutoRoute(
|
||||
page: AppLogDetailPage,
|
||||
),
|
||||
|
||||
@@ -72,6 +72,7 @@ class _$AppRouter extends RootStackRouter {
|
||||
totalAssets: args.totalAssets,
|
||||
heroOffset: args.heroOffset,
|
||||
showStack: args.showStack,
|
||||
isOwner: args.isOwner,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -230,12 +231,9 @@ class _$AppRouter extends RootStackRouter {
|
||||
);
|
||||
},
|
||||
AppLogRoute.name: (routeData) {
|
||||
return CustomPage<dynamic>(
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const AppLogPage(),
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
AppLogDetailRoute.name: (routeData) {
|
||||
@@ -582,6 +580,7 @@ class _$AppRouter extends RootStackRouter {
|
||||
RouteConfig(
|
||||
AppLogRoute.name,
|
||||
path: '/app-log-page',
|
||||
guards: [duplicateGuard],
|
||||
),
|
||||
RouteConfig(
|
||||
AppLogDetailRoute.name,
|
||||
@@ -749,6 +748,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||
required int totalAssets,
|
||||
int heroOffset = 0,
|
||||
bool showStack = false,
|
||||
bool isOwner = true,
|
||||
}) : super(
|
||||
GalleryViewerRoute.name,
|
||||
path: '/gallery-viewer-page',
|
||||
@@ -759,6 +759,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||
totalAssets: totalAssets,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
isOwner: isOwner,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -773,6 +774,7 @@ class GalleryViewerRouteArgs {
|
||||
required this.totalAssets,
|
||||
this.heroOffset = 0,
|
||||
this.showStack = false,
|
||||
this.isOwner = true,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
@@ -787,9 +789,11 @@ class GalleryViewerRouteArgs {
|
||||
|
||||
final bool showStack;
|
||||
|
||||
final bool isOwner;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack}';
|
||||
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack, isOwner: $isOwner}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
|
||||
part 'exif_info.g.dart';
|
||||
|
||||
@@ -165,7 +164,11 @@ double? _exposureTimeToSeconds(String? s) {
|
||||
}
|
||||
final parts = s.split("/");
|
||||
if (parts.length == 2) {
|
||||
return parts[0].toDouble() / parts[1].toDouble();
|
||||
final numerator = double.tryParse(parts[0]);
|
||||
final denominator = double.tryParse(parts[1]);
|
||||
if (numerator != null && denominator != null) {
|
||||
return numerator / denominator;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
263
mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
Normal file
263
mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
Normal file
@@ -0,0 +1,263 @@
|
||||
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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
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/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_profile_info.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_server_info.dart';
|
||||
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
const ImmichAppBarDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final theme = Theme.of(context);
|
||||
bool isDarkTheme = theme.brightness == Brightness.dark;
|
||||
bool isHorizontal = MediaQuery.of(context).size.width > 600;
|
||||
final horizontalPadding = isHorizontal ? 100.0 : 20.0;
|
||||
final user = ref.watch(currentUserProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(backupProvider.notifier).updateServerInfo();
|
||||
return null;
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
buildTopRow() {
|
||||
return Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
buildActionButton(IconData icon, String text, Function() onTap) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.standard,
|
||||
contentPadding: const EdgeInsets.only(left: 30),
|
||||
minLeadingWidth: 40,
|
||||
leading: SizedBox(
|
||||
child: Icon(
|
||||
icon,
|
||||
color: theme.textTheme.labelMedium?.color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
text,
|
||||
style:
|
||||
theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
buildSettingButton() {
|
||||
return buildActionButton(
|
||||
Icons.settings_rounded,
|
||||
"profile_drawer_settings",
|
||||
() => AutoRouter.of(context).push(const SettingsRoute()),
|
||||
);
|
||||
}
|
||||
|
||||
buildAppLogButton() {
|
||||
return buildActionButton(
|
||||
Icons.assignment_outlined,
|
||||
"profile_drawer_app_logs",
|
||||
() => AutoRouter.of(context).push(const AppLogRoute()),
|
||||
);
|
||||
}
|
||||
|
||||
buildSignOutButton() {
|
||||
return buildActionButton(
|
||||
Icons.logout_rounded,
|
||||
"profile_drawer_sign_out",
|
||||
() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return ConfirmDialog(
|
||||
title: "app_bar_signout_dialog_title",
|
||||
content: "app_bar_signout_dialog_content",
|
||||
ok: "app_bar_signout_dialog_ok",
|
||||
onOk: () async {
|
||||
await ref.watch(authenticationProvider.notifier).logout();
|
||||
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.watch(backupProvider.notifier).cancelBackup();
|
||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
AutoRouter.of(context).replace(const LoginRoute());
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStorageInformation() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: const Color.fromARGB(255, 225, 229, 240),
|
||||
),
|
||||
child: ListTile(
|
||||
minLeadingWidth: 50,
|
||||
leading: Icon(
|
||||
Icons.storage_rounded,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
title: const Text(
|
||||
"backup_controller_page_server_storage",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
isThreeLine: true,
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 5.0,
|
||||
value: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||
backgroundColor: Colors.grey,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child:
|
||||
const Text('backup_controller_page_storage_format').tr(
|
||||
args: [
|
||||
backupState.serverInfo.diskUse,
|
||||
backupState.serverInfo.diskSize,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildFooter() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
launchUrl(
|
||||
Uri.parse('https://immich.app'),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"profile_drawer_documentation",
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr(),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
child: Text(
|
||||
"•",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
launchUrl(
|
||||
Uri.parse('https://github.com/immich-app/immich'),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"profile_drawer_github",
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: Alignment.topCenter,
|
||||
insetPadding: EdgeInsets.only(
|
||||
top: isHorizontal ? 20 : 60,
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
bottom: isHorizontal ? 20 : 100,
|
||||
),
|
||||
backgroundColor: theme.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: buildTopRow(),
|
||||
),
|
||||
const AppBarProfileInfoBox(),
|
||||
buildStorageInformation(),
|
||||
const AppBarServerInfo(),
|
||||
buildAppLogButton(),
|
||||
buildSettingButton(),
|
||||
buildSignOutButton(),
|
||||
buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||
@@ -9,8 +8,8 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
const ProfileDrawerHeader({
|
||||
class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||
const AppBarProfileInfoBox({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -23,30 +22,24 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
buildUserProfileImage() {
|
||||
const immichImage = CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
|
||||
if (authState.profileImagePath.isEmpty || user == null) {
|
||||
return const CircleAvatar(
|
||||
radius: 35,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
return immichImage;
|
||||
}
|
||||
|
||||
var userImage = UserCircleAvatar(
|
||||
radius: 35,
|
||||
size: 66,
|
||||
final userImage = UserCircleAvatar(
|
||||
radius: 20,
|
||||
size: 40,
|
||||
user: user,
|
||||
);
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||
if (authState.profileImagePath.isNotEmpty) {
|
||||
return userImage;
|
||||
} else {
|
||||
return const CircleAvatar(
|
||||
radius: 33,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
return authState.profileImagePath.isNotEmpty ? userImage : immichImage;
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||
@@ -54,18 +47,18 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
||||
return const CircleAvatar(
|
||||
radius: 35,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
return immichImage;
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||
return const ImmichLoadingIndicator();
|
||||
return const SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: ImmichLoadingIndicator(borderRadius: 20),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
return immichImage;
|
||||
}
|
||||
|
||||
pickUserProfileImage() async {
|
||||
@@ -80,54 +73,45 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||
|
||||
if (success) {
|
||||
final profileImagePath =
|
||||
ref.read(uploadProfileImageProvider).profileImagePath;
|
||||
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||
ref.read(uploadProfileImageProvider).profileImagePath,
|
||||
profileImagePath,
|
||||
);
|
||||
if (user != null) {
|
||||
user.profileImagePath = profileImagePath;
|
||||
Store.put(StoreKey.currentUser, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
// buildUserProfileImage();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return DrawerHeader(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isDarkMode
|
||||
? [
|
||||
const Color.fromARGB(255, 22, 25, 48),
|
||||
const Color.fromARGB(255, 13, 13, 13),
|
||||
const Color.fromARGB(255, 0, 0, 0),
|
||||
]
|
||||
: [
|
||||
const Color.fromARGB(255, 216, 219, 238),
|
||||
const Color.fromARGB(255, 242, 242, 242),
|
||||
Colors.white,
|
||||
],
|
||||
begin: Alignment.centerRight,
|
||||
end: Alignment.centerLeft,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: const Color.fromARGB(255, 225, 229, 240),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: ListTile(
|
||||
minLeadingWidth: 50,
|
||||
leading: GestureDetector(
|
||||
onTap: pickUserProfileImage,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
buildUserProfileImage(),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: -5,
|
||||
bottom: -5,
|
||||
right: -8,
|
||||
child: Material(
|
||||
color: isDarkMode ? Colors.grey[700] : Colors.grey[100],
|
||||
color: isDarkMode ? Colors.blueGrey[800] : Colors.white,
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
@@ -135,7 +119,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
Icons.camera_alt_outlined,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 14,
|
||||
),
|
||||
@@ -145,19 +129,21 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title: Text(
|
||||
"${authState.firstName} ${authState.lastName}",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle: Text(
|
||||
authState.userEmail,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
209
mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
Normal file
209
mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AppBarServerInfo extends HookConsumerWidget {
|
||||
const AppBarServerInfo({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
|
||||
final appInfo = useState({});
|
||||
|
||||
getPackageInfo() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
appInfo.value = {
|
||||
"version": packageInfo.version,
|
||||
"buildNumber": packageInfo.buildNumber,
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
getPackageInfo();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: const Color.fromARGB(255, 225, 229, 240),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
serverInfoState.isVersionMismatch
|
||||
? serverInfoState.versionMismatchErrorMessage
|
||||
: "profile_drawer_client_server_up_to_date".tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Divider(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
thickness: 1,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Text(
|
||||
"server_info_box_app_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Divider(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
thickness: 1,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Text(
|
||||
"server_info_box_server_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
serverInfoState.serverVersion.major > 0
|
||||
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
|
||||
: "?",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Divider(
|
||||
color: Color.fromARGB(101, 201, 201, 201),
|
||||
thickness: 1,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Text(
|
||||
"server_info_box_server_url".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Container(
|
||||
width: 200,
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
getServerUrl() ?? '--',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
mobile/lib/shared/ui/immich_app_bar.dart
Normal file
192
mobile/lib/shared/ui/immich_app_bar.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
|
||||
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
final Widget? action;
|
||||
|
||||
const ImmichAppBar({super.key, this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final BackUpState backupState = ref.watch(backupProvider);
|
||||
final bool isEnableAutoBackup =
|
||||
backupState.backgroundBackup || backupState.autoBackup;
|
||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
const widgetSize = 30.0;
|
||||
|
||||
buildProfilePhoto() {
|
||||
return InkWell(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (ctx) => const ImmichAppBarDialog(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: authState.profileImagePath.isEmpty || user == null
|
||||
? const Icon(
|
||||
Icons.face_outlined,
|
||||
size: widgetSize,
|
||||
)
|
||||
: UserCircleAvatar(
|
||||
radius: 15,
|
||||
size: 27,
|
||||
user: user,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildProfileIndicator() {
|
||||
return Badge(
|
||||
label: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(widgetSize / 2),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.info,
|
||||
color: Color.fromARGB(255, 243, 188, 106),
|
||||
size: widgetSize / 2,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
alignment: Alignment.bottomRight,
|
||||
isLabelVisible: serverInfoState.isVersionMismatch,
|
||||
offset: const Offset(2, 2),
|
||||
child: buildProfilePhoto(),
|
||||
);
|
||||
}
|
||||
|
||||
getBackupBadgeIcon() {
|
||||
final iconColor = isDarkMode ? Colors.white : Colors.black;
|
||||
|
||||
if (isEnableAutoBackup) {
|
||||
if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(3.5),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
strokeCap: StrokeCap.round,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
|
||||
),
|
||||
);
|
||||
} else if (backupState.backupProgress !=
|
||||
BackUpProgressEnum.inBackground &&
|
||||
backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
return Icon(
|
||||
Icons.check_outlined,
|
||||
size: 9,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEnableAutoBackup) {
|
||||
return Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 9,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buildBackupIndicator() {
|
||||
final indicatorIcon = getBackupBadgeIcon();
|
||||
final badgeBackground = isDarkMode ? Colors.blueGrey[800] : Colors.white;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => AutoRouter.of(context).push(const BackupControllerRoute()),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Badge(
|
||||
label: Container(
|
||||
width: widgetSize / 2,
|
||||
height: widgetSize / 2,
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBackground,
|
||||
border: Border.all(
|
||||
color: isDarkMode ? Colors.black : Colors.grey,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(widgetSize / 2),
|
||||
),
|
||||
child: indicatorIcon,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
alignment: Alignment.bottomRight,
|
||||
isLabelVisible: indicatorIcon != null,
|
||||
offset: const Offset(2, 2),
|
||||
child: Icon(
|
||||
Icons.backup_rounded,
|
||||
size: widgetSize,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: Image.asset(
|
||||
'assets/immich-logo.png',
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 10),
|
||||
child: const Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (action != null)
|
||||
Padding(padding: const EdgeInsets.only(right: 20), child: action!),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildBackupIndicator(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildProfileIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImmichLoadingIndicator extends StatelessWidget {
|
||||
final double? borderRadius;
|
||||
|
||||
const ImmichLoadingIndicator({
|
||||
Key? key,
|
||||
this.borderRadius,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -12,7 +15,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
|
||||
width: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 10),
|
||||
),
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: const CircularProgressIndicator(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
@@ -46,7 +47,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
radius: radius,
|
||||
child: user.profileImagePath == ""
|
||||
? Text(
|
||||
user.firstName[0],
|
||||
user.firstName[0].toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
@@ -54,19 +55,18 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: FadeInImage(
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
cacheKey: user.profileImagePath,
|
||||
width: size,
|
||||
height: size,
|
||||
image: NetworkImage(
|
||||
profileImageUrl,
|
||||
headers: {
|
||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
|
||||
},
|
||||
),
|
||||
fadeInDuration: const Duration(milliseconds: 200),
|
||||
imageErrorBuilder: (context, error, stackTrace) =>
|
||||
placeholder: (_, __) => Image.memory(kTransparentImage),
|
||||
imageUrl: profileImageUrl,
|
||||
httpHeaders: {
|
||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
|
||||
},
|
||||
fadeInDuration: const Duration(milliseconds: 300),
|
||||
errorWidget: (context, error, stackTrace) =>
|
||||
Image.memory(kTransparentImage),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
String getThumbnailUrl(
|
||||
@@ -35,8 +36,10 @@ String getAlbumThumbnailUrl(
|
||||
if (album.thumbnail.value?.remoteId == null) {
|
||||
return '';
|
||||
}
|
||||
return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!,
|
||||
type: type,);
|
||||
return getThumbnailUrlForRemoteId(
|
||||
album.thumbnail.value!.remoteId!,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
String getAlbumThumbNailCacheKey(
|
||||
@@ -57,7 +60,9 @@ String getImageUrl(final Asset asset) {
|
||||
}
|
||||
|
||||
String getImageCacheKey(final Asset asset) {
|
||||
return '${asset.id}_fullStage';
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromDto = asset.id == Isar.autoIncrement;
|
||||
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
|
||||
}
|
||||
|
||||
String getThumbnailUrlForRemoteId(
|
||||
|
||||
27
mobile/openapi/.openapi-generator/FILES
generated
27
mobile/openapi/.openapi-generator/FILES
generated
@@ -8,6 +8,10 @@ doc/APIKeyCreateDto.md
|
||||
doc/APIKeyCreateResponseDto.md
|
||||
doc/APIKeyResponseDto.md
|
||||
doc/APIKeyUpdateDto.md
|
||||
doc/ActivityApi.md
|
||||
doc/ActivityCreateDto.md
|
||||
doc/ActivityResponseDto.md
|
||||
doc/ActivityStatisticsResponseDto.md
|
||||
doc/AddUsersDto.md
|
||||
doc/AdminSignupResponseDto.md
|
||||
doc/AlbumApi.md
|
||||
@@ -97,6 +101,7 @@ doc/PersonResponseDto.md
|
||||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionType.md
|
||||
doc/RecognitionConfig.md
|
||||
doc/ScanLibraryDto.md
|
||||
doc/SearchAlbumResponseDto.md
|
||||
@@ -128,6 +133,8 @@ doc/SystemConfigApi.md
|
||||
doc/SystemConfigDto.md
|
||||
doc/SystemConfigFFmpegDto.md
|
||||
doc/SystemConfigJobDto.md
|
||||
doc/SystemConfigLibraryDto.md
|
||||
doc/SystemConfigLibraryScanDto.md
|
||||
doc/SystemConfigMachineLearningDto.md
|
||||
doc/SystemConfigMapDto.md
|
||||
doc/SystemConfigNewVersionCheckDto.md
|
||||
@@ -156,12 +163,13 @@ doc/UpdateTagDto.md
|
||||
doc/UpdateUserDto.md
|
||||
doc/UsageByUserDto.md
|
||||
doc/UserApi.md
|
||||
doc/UserCountResponseDto.md
|
||||
doc/UserDto.md
|
||||
doc/UserResponseDto.md
|
||||
doc/ValidateAccessTokenResponseDto.md
|
||||
doc/VideoCodec.md
|
||||
git_push.sh
|
||||
lib/api.dart
|
||||
lib/api/activity_api.dart
|
||||
lib/api/album_api.dart
|
||||
lib/api/api_key_api.dart
|
||||
lib/api/asset_api.dart
|
||||
@@ -186,6 +194,9 @@ lib/auth/authentication.dart
|
||||
lib/auth/http_basic_auth.dart
|
||||
lib/auth/http_bearer_auth.dart
|
||||
lib/auth/oauth.dart
|
||||
lib/model/activity_create_dto.dart
|
||||
lib/model/activity_response_dto.dart
|
||||
lib/model/activity_statistics_response_dto.dart
|
||||
lib/model/add_users_dto.dart
|
||||
lib/model/admin_signup_response_dto.dart
|
||||
lib/model/album_count_response_dto.dart
|
||||
@@ -270,6 +281,7 @@ lib/model/person_response_dto.dart
|
||||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_type.dart
|
||||
lib/model/recognition_config.dart
|
||||
lib/model/scan_library_dto.dart
|
||||
lib/model/search_album_response_dto.dart
|
||||
@@ -297,6 +309,8 @@ lib/model/smart_info_response_dto.dart
|
||||
lib/model/system_config_dto.dart
|
||||
lib/model/system_config_f_fmpeg_dto.dart
|
||||
lib/model/system_config_job_dto.dart
|
||||
lib/model/system_config_library_dto.dart
|
||||
lib/model/system_config_library_scan_dto.dart
|
||||
lib/model/system_config_machine_learning_dto.dart
|
||||
lib/model/system_config_map_dto.dart
|
||||
lib/model/system_config_new_version_check_dto.dart
|
||||
@@ -323,11 +337,15 @@ lib/model/update_stack_parent_dto.dart
|
||||
lib/model/update_tag_dto.dart
|
||||
lib/model/update_user_dto.dart
|
||||
lib/model/usage_by_user_dto.dart
|
||||
lib/model/user_count_response_dto.dart
|
||||
lib/model/user_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
lib/model/video_codec.dart
|
||||
pubspec.yaml
|
||||
test/activity_api_test.dart
|
||||
test/activity_create_dto_test.dart
|
||||
test/activity_response_dto_test.dart
|
||||
test/activity_statistics_response_dto_test.dart
|
||||
test/add_users_dto_test.dart
|
||||
test/admin_signup_response_dto_test.dart
|
||||
test/album_api_test.dart
|
||||
@@ -422,6 +440,7 @@ test/person_response_dto_test.dart
|
||||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_type_test.dart
|
||||
test/recognition_config_test.dart
|
||||
test/scan_library_dto_test.dart
|
||||
test/search_album_response_dto_test.dart
|
||||
@@ -453,6 +472,8 @@ test/system_config_api_test.dart
|
||||
test/system_config_dto_test.dart
|
||||
test/system_config_f_fmpeg_dto_test.dart
|
||||
test/system_config_job_dto_test.dart
|
||||
test/system_config_library_dto_test.dart
|
||||
test/system_config_library_scan_dto_test.dart
|
||||
test/system_config_machine_learning_dto_test.dart
|
||||
test/system_config_map_dto_test.dart
|
||||
test/system_config_new_version_check_dto_test.dart
|
||||
@@ -481,7 +502,7 @@ test/update_tag_dto_test.dart
|
||||
test/update_user_dto_test.dart
|
||||
test/usage_by_user_dto_test.dart
|
||||
test/user_api_test.dart
|
||||
test/user_count_response_dto_test.dart
|
||||
test/user_dto_test.dart
|
||||
test/user_response_dto_test.dart
|
||||
test/validate_access_token_response_dto_test.dart
|
||||
test/video_codec_test.dart
|
||||
|
||||
15
mobile/openapi/README.md
generated
15
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.83.0
|
||||
- API version: 1.84.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -77,6 +77,10 @@ Class | Method | HTTP request | Description
|
||||
*APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} |
|
||||
*APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key |
|
||||
*APIKeyApi* | [**updateKey**](doc//APIKeyApi.md#updatekey) | **PUT** /api-key/{id} |
|
||||
*ActivityApi* | [**createActivity**](doc//ActivityApi.md#createactivity) | **POST** /activity |
|
||||
*ActivityApi* | [**deleteActivity**](doc//ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
|
||||
*ActivityApi* | [**getActivities**](doc//ActivityApi.md#getactivities) | **GET** /activity |
|
||||
*ActivityApi* | [**getActivityStatistics**](doc//ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
|
||||
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
|
||||
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
|
||||
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
|
||||
@@ -194,7 +198,6 @@ Class | Method | HTTP request | Description
|
||||
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} |
|
||||
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{id} |
|
||||
*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count |
|
||||
*UserApi* | [**restoreUser**](doc//UserApi.md#restoreuser) | **POST** /user/{id}/restore |
|
||||
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user |
|
||||
|
||||
@@ -205,6 +208,9 @@ Class | Method | HTTP request | Description
|
||||
- [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md)
|
||||
- [APIKeyResponseDto](doc//APIKeyResponseDto.md)
|
||||
- [APIKeyUpdateDto](doc//APIKeyUpdateDto.md)
|
||||
- [ActivityCreateDto](doc//ActivityCreateDto.md)
|
||||
- [ActivityResponseDto](doc//ActivityResponseDto.md)
|
||||
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
|
||||
- [AddUsersDto](doc//AddUsersDto.md)
|
||||
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
|
||||
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
||||
@@ -285,6 +291,7 @@ Class | Method | HTTP request | Description
|
||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
- [RecognitionConfig](doc//RecognitionConfig.md)
|
||||
- [ScanLibraryDto](doc//ScanLibraryDto.md)
|
||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||
@@ -312,6 +319,8 @@ Class | Method | HTTP request | Description
|
||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
||||
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
||||
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
||||
- [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md)
|
||||
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
|
||||
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
|
||||
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
|
||||
@@ -338,7 +347,7 @@ Class | Method | HTTP request | Description
|
||||
- [UpdateTagDto](doc//UpdateTagDto.md)
|
||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserCountResponseDto](doc//UserCountResponseDto.md)
|
||||
- [UserDto](doc//UserDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
- [VideoCodec](doc//VideoCodec.md)
|
||||
|
||||
244
mobile/openapi/doc/ActivityApi.md
generated
Normal file
244
mobile/openapi/doc/ActivityApi.md
generated
Normal file
@@ -0,0 +1,244 @@
|
||||
# openapi.api.ActivityApi
|
||||
|
||||
## Load the API package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**createActivity**](ActivityApi.md#createactivity) | **POST** /activity |
|
||||
[**deleteActivity**](ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
|
||||
[**getActivities**](ActivityApi.md#getactivities) | **GET** /activity |
|
||||
[**getActivityStatistics**](ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
|
||||
|
||||
|
||||
# **createActivity**
|
||||
> ActivityResponseDto createActivity(activityCreateDto)
|
||||
|
||||
|
||||
|
||||
### 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 = ActivityApi();
|
||||
final activityCreateDto = ActivityCreateDto(); // ActivityCreateDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.createActivity(activityCreateDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ActivityApi->createActivity: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**activityCreateDto** | [**ActivityCreateDto**](ActivityCreateDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**ActivityResponseDto**](ActivityResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **deleteActivity**
|
||||
> deleteActivity(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = ActivityApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
api_instance.deleteActivity(id);
|
||||
} catch (e) {
|
||||
print('Exception when calling ActivityApi->deleteActivity: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### 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)
|
||||
|
||||
# **getActivities**
|
||||
> List<ActivityResponseDto> getActivities(albumId, assetId, type, userId)
|
||||
|
||||
|
||||
|
||||
### 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 = ActivityApi();
|
||||
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final type = ; // ReactionType |
|
||||
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getActivities(albumId, assetId, type, userId);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ActivityApi->getActivities: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**albumId** | **String**| |
|
||||
**assetId** | **String**| | [optional]
|
||||
**type** | [**ReactionType**](.md)| | [optional]
|
||||
**userId** | **String**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<ActivityResponseDto>**](ActivityResponseDto.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)
|
||||
|
||||
# **getActivityStatistics**
|
||||
> ActivityStatisticsResponseDto getActivityStatistics(albumId, assetId)
|
||||
|
||||
|
||||
|
||||
### 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 = ActivityApi();
|
||||
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getActivityStatistics(albumId, assetId);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ActivityApi->getActivityStatistics: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**albumId** | **String**| |
|
||||
**assetId** | **String**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
[**ActivityStatisticsResponseDto**](ActivityStatisticsResponseDto.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)
|
||||
|
||||
18
mobile/openapi/doc/ActivityCreateDto.md
generated
Normal file
18
mobile/openapi/doc/ActivityCreateDto.md
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
# openapi.model.ActivityCreateDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**albumId** | **String** | |
|
||||
**assetId** | **String** | | [optional]
|
||||
**comment** | **String** | | [optional]
|
||||
**type** | [**ReactionType**](ReactionType.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)
|
||||
|
||||
|
||||
20
mobile/openapi/doc/ActivityResponseDto.md
generated
Normal file
20
mobile/openapi/doc/ActivityResponseDto.md
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
# openapi.model.ActivityResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetId** | **String** | |
|
||||
**comment** | **String** | | [optional]
|
||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||
**id** | **String** | |
|
||||
**type** | **String** | |
|
||||
**user** | [**UserDto**](UserDto.md) | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
15
mobile/openapi/doc/ActivityStatisticsResponseDto.md
generated
Normal file
15
mobile/openapi/doc/ActivityStatisticsResponseDto.md
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.ActivityStatisticsResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**comments** | **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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# openapi.model.UserCountResponseDto
|
||||
# openapi.model.ReactionType
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**userCount** | **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)
|
||||
|
||||
8
mobile/openapi/doc/SharedLinkApi.md
generated
8
mobile/openapi/doc/SharedLinkApi.md
generated
@@ -185,7 +185,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)
|
||||
|
||||
# **getMySharedLink**
|
||||
> SharedLinkResponseDto getMySharedLink(key)
|
||||
> SharedLinkResponseDto getMySharedLink(password, token, key)
|
||||
|
||||
|
||||
|
||||
@@ -208,10 +208,12 @@ import 'package:openapi/api.dart';
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = SharedLinkApi();
|
||||
final password = password; // String |
|
||||
final token = token_example; // String |
|
||||
final key = key_example; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getMySharedLink(key);
|
||||
final result = api_instance.getMySharedLink(password, token, key);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling SharedLinkApi->getMySharedLink: $e\n');
|
||||
@@ -222,6 +224,8 @@ try {
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**password** | **String**| | [optional]
|
||||
**token** | **String**| | [optional]
|
||||
**key** | **String**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
1
mobile/openapi/doc/SharedLinkCreateDto.md
generated
1
mobile/openapi/doc/SharedLinkCreateDto.md
generated
@@ -14,6 +14,7 @@ Name | Type | Description | Notes
|
||||
**assetIds** | **List<String>** | | [optional] [default to const []]
|
||||
**description** | **String** | | [optional]
|
||||
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**password** | **String** | | [optional]
|
||||
**showMetadata** | **bool** | | [optional] [default to true]
|
||||
**type** | [**SharedLinkType**](SharedLinkType.md) | |
|
||||
|
||||
|
||||
1
mobile/openapi/doc/SharedLinkEditDto.md
generated
1
mobile/openapi/doc/SharedLinkEditDto.md
generated
@@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
||||
**changeExpiryTime** | **bool** | Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. | [optional]
|
||||
**description** | **String** | | [optional]
|
||||
**expiresAt** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**password** | **String** | | [optional]
|
||||
**showMetadata** | **bool** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
2
mobile/openapi/doc/SharedLinkResponseDto.md
generated
2
mobile/openapi/doc/SharedLinkResponseDto.md
generated
@@ -17,7 +17,9 @@ Name | Type | Description | Notes
|
||||
**expiresAt** | [**DateTime**](DateTime.md) | |
|
||||
**id** | **String** | |
|
||||
**key** | **String** | |
|
||||
**password** | **String** | |
|
||||
**showMetadata** | **bool** | |
|
||||
**token** | **String** | | [optional]
|
||||
**type** | [**SharedLinkType**](SharedLinkType.md) | |
|
||||
**userId** | **String** | |
|
||||
|
||||
|
||||
1
mobile/openapi/doc/SystemConfigDto.md
generated
1
mobile/openapi/doc/SystemConfigDto.md
generated
@@ -10,6 +10,7 @@ Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | |
|
||||
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | |
|
||||
**library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | |
|
||||
**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | |
|
||||
**map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | |
|
||||
**newVersionCheck** | [**SystemConfigNewVersionCheckDto**](SystemConfigNewVersionCheckDto.md) | |
|
||||
|
||||
15
mobile/openapi/doc/SystemConfigLibraryDto.md
generated
Normal file
15
mobile/openapi/doc/SystemConfigLibraryDto.md
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.SystemConfigLibraryDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**scan** | [**SystemConfigLibraryScanDto**](SystemConfigLibraryScanDto.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)
|
||||
|
||||
|
||||
16
mobile/openapi/doc/SystemConfigLibraryScanDto.md
generated
Normal file
16
mobile/openapi/doc/SystemConfigLibraryScanDto.md
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.SystemConfigLibraryScanDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**cronExpression** | **String** | |
|
||||
**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)
|
||||
|
||||
|
||||
56
mobile/openapi/doc/UserApi.md
generated
56
mobile/openapi/doc/UserApi.md
generated
@@ -16,7 +16,6 @@ Method | HTTP request | Description
|
||||
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} |
|
||||
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{id} |
|
||||
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count |
|
||||
[**restoreUser**](UserApi.md#restoreuser) | **POST** /user/{id}/restore |
|
||||
[**updateUser**](UserApi.md#updateuser) | **PUT** /user |
|
||||
|
||||
@@ -402,61 +401,6 @@ 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)
|
||||
|
||||
# **getUserCount**
|
||||
> UserCountResponseDto getUserCount(admin)
|
||||
|
||||
|
||||
|
||||
### 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 |
|
||||
|
||||
try {
|
||||
final result = api_instance.getUserCount(admin);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling UserApi->getUserCount: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**admin** | **bool**| | [optional] [default to false]
|
||||
|
||||
### Return type
|
||||
|
||||
[**UserCountResponseDto**](UserCountResponseDto.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)
|
||||
|
||||
# **restoreUser**
|
||||
> UserResponseDto restoreUser(id)
|
||||
|
||||
|
||||
19
mobile/openapi/doc/UserDto.md
generated
Normal file
19
mobile/openapi/doc/UserDto.md
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
# openapi.model.UserDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**email** | **String** | |
|
||||
**firstName** | **String** | |
|
||||
**id** | **String** | |
|
||||
**lastName** | **String** | |
|
||||
**profileImagePath** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
9
mobile/openapi/lib/api.dart
generated
9
mobile/openapi/lib/api.dart
generated
@@ -29,6 +29,7 @@ part 'auth/http_basic_auth.dart';
|
||||
part 'auth/http_bearer_auth.dart';
|
||||
|
||||
part 'api/api_key_api.dart';
|
||||
part 'api/activity_api.dart';
|
||||
part 'api/album_api.dart';
|
||||
part 'api/asset_api.dart';
|
||||
part 'api/audit_api.dart';
|
||||
@@ -49,6 +50,9 @@ part 'model/api_key_create_dto.dart';
|
||||
part 'model/api_key_create_response_dto.dart';
|
||||
part 'model/api_key_response_dto.dart';
|
||||
part 'model/api_key_update_dto.dart';
|
||||
part 'model/activity_create_dto.dart';
|
||||
part 'model/activity_response_dto.dart';
|
||||
part 'model/activity_statistics_response_dto.dart';
|
||||
part 'model/add_users_dto.dart';
|
||||
part 'model/admin_signup_response_dto.dart';
|
||||
part 'model/album_count_response_dto.dart';
|
||||
@@ -129,6 +133,7 @@ part 'model/person_response_dto.dart';
|
||||
part 'model/person_statistics_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
part 'model/recognition_config.dart';
|
||||
part 'model/scan_library_dto.dart';
|
||||
part 'model/search_album_response_dto.dart';
|
||||
@@ -156,6 +161,8 @@ part 'model/smart_info_response_dto.dart';
|
||||
part 'model/system_config_dto.dart';
|
||||
part 'model/system_config_f_fmpeg_dto.dart';
|
||||
part 'model/system_config_job_dto.dart';
|
||||
part 'model/system_config_library_dto.dart';
|
||||
part 'model/system_config_library_scan_dto.dart';
|
||||
part 'model/system_config_machine_learning_dto.dart';
|
||||
part 'model/system_config_map_dto.dart';
|
||||
part 'model/system_config_new_version_check_dto.dart';
|
||||
@@ -182,7 +189,7 @@ part 'model/update_stack_parent_dto.dart';
|
||||
part 'model/update_tag_dto.dart';
|
||||
part 'model/update_user_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_count_response_dto.dart';
|
||||
part 'model/user_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
part 'model/video_codec.dart';
|
||||
|
||||
234
mobile/openapi/lib/api/activity_api.dart
generated
Normal file
234
mobile/openapi/lib/api/activity_api.dart
generated
Normal file
@@ -0,0 +1,234 @@
|
||||
//
|
||||
// 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 ActivityApi {
|
||||
ActivityApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'POST /activity' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [ActivityCreateDto] activityCreateDto (required):
|
||||
Future<Response> createActivityWithHttpInfo(ActivityCreateDto activityCreateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/activity';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = activityCreateDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [ActivityCreateDto] activityCreateDto (required):
|
||||
Future<ActivityResponseDto?> createActivity(ActivityCreateDto activityCreateDto,) async {
|
||||
final response = await createActivityWithHttpInfo(activityCreateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityResponseDto',) as ActivityResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /activity/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteActivityWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/activity/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deleteActivity(String id,) async {
|
||||
final response = await deleteActivityWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /activity' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
///
|
||||
/// * [String] assetId:
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
///
|
||||
/// * [String] userId:
|
||||
Future<Response> getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, String? userId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/activity';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||
if (assetId != null) {
|
||||
queryParams.addAll(_queryParams('', 'assetId', assetId));
|
||||
}
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
if (userId != null) {
|
||||
queryParams.addAll(_queryParams('', 'userId', userId));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
///
|
||||
/// * [String] assetId:
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
///
|
||||
/// * [String] userId:
|
||||
Future<List<ActivityResponseDto>?> getActivities(String albumId, { String? assetId, ReactionType? type, String? userId, }) async {
|
||||
final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, userId: userId, );
|
||||
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<ActivityResponseDto>') as List)
|
||||
.cast<ActivityResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /activity/statistics' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
///
|
||||
/// * [String] assetId:
|
||||
Future<Response> getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/activity/statistics';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||
if (assetId != null) {
|
||||
queryParams.addAll(_queryParams('', 'assetId', assetId));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
///
|
||||
/// * [String] assetId:
|
||||
Future<ActivityStatisticsResponseDto?> getActivityStatistics(String albumId, { String? assetId, }) async {
|
||||
final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityStatisticsResponseDto',) as ActivityStatisticsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
mobile/openapi/lib/api/shared_link_api.dart
generated
20
mobile/openapi/lib/api/shared_link_api.dart
generated
@@ -173,8 +173,12 @@ class SharedLinkApi {
|
||||
/// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] password:
|
||||
///
|
||||
/// * [String] token:
|
||||
///
|
||||
/// * [String] key:
|
||||
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async {
|
||||
Future<Response> getMySharedLinkWithHttpInfo({ String? password, String? token, String? key, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/shared-link/me';
|
||||
|
||||
@@ -185,6 +189,12 @@ class SharedLinkApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (password != null) {
|
||||
queryParams.addAll(_queryParams('', 'password', password));
|
||||
}
|
||||
if (token != null) {
|
||||
queryParams.addAll(_queryParams('', 'token', token));
|
||||
}
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
@@ -205,9 +215,13 @@ class SharedLinkApi {
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] password:
|
||||
///
|
||||
/// * [String] token:
|
||||
///
|
||||
/// * [String] key:
|
||||
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async {
|
||||
final response = await getMySharedLinkWithHttpInfo( key: key, );
|
||||
Future<SharedLinkResponseDto?> getMySharedLink({ String? password, String? token, String? key, }) async {
|
||||
final response = await getMySharedLinkWithHttpInfo( password: password, token: token, key: key, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
51
mobile/openapi/lib/api/user_api.dart
generated
51
mobile/openapi/lib/api/user_api.dart
generated
@@ -357,57 +357,6 @@ class UserApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /user/count' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [bool] admin:
|
||||
Future<Response> getUserCountWithHttpInfo({ bool? admin, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/user/count';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (admin != null) {
|
||||
queryParams.addAll(_queryParams('', 'admin', admin));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [bool] admin:
|
||||
Future<UserCountResponseDto?> getUserCount({ bool? admin, }) async {
|
||||
final response = await getUserCountWithHttpInfo( admin: admin, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserCountResponseDto',) as UserCountResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /user/{id}/restore' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
16
mobile/openapi/lib/api_client.dart
generated
16
mobile/openapi/lib/api_client.dart
generated
@@ -189,6 +189,12 @@ class ApiClient {
|
||||
return APIKeyResponseDto.fromJson(value);
|
||||
case 'APIKeyUpdateDto':
|
||||
return APIKeyUpdateDto.fromJson(value);
|
||||
case 'ActivityCreateDto':
|
||||
return ActivityCreateDto.fromJson(value);
|
||||
case 'ActivityResponseDto':
|
||||
return ActivityResponseDto.fromJson(value);
|
||||
case 'ActivityStatisticsResponseDto':
|
||||
return ActivityStatisticsResponseDto.fromJson(value);
|
||||
case 'AddUsersDto':
|
||||
return AddUsersDto.fromJson(value);
|
||||
case 'AdminSignupResponseDto':
|
||||
@@ -349,6 +355,8 @@ class ApiClient {
|
||||
return PersonUpdateDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'ReactionType':
|
||||
return ReactionTypeTypeTransformer().decode(value);
|
||||
case 'RecognitionConfig':
|
||||
return RecognitionConfig.fromJson(value);
|
||||
case 'ScanLibraryDto':
|
||||
@@ -403,6 +411,10 @@ class ApiClient {
|
||||
return SystemConfigFFmpegDto.fromJson(value);
|
||||
case 'SystemConfigJobDto':
|
||||
return SystemConfigJobDto.fromJson(value);
|
||||
case 'SystemConfigLibraryDto':
|
||||
return SystemConfigLibraryDto.fromJson(value);
|
||||
case 'SystemConfigLibraryScanDto':
|
||||
return SystemConfigLibraryScanDto.fromJson(value);
|
||||
case 'SystemConfigMachineLearningDto':
|
||||
return SystemConfigMachineLearningDto.fromJson(value);
|
||||
case 'SystemConfigMapDto':
|
||||
@@ -455,8 +467,8 @@ class ApiClient {
|
||||
return UpdateUserDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
return UsageByUserDto.fromJson(value);
|
||||
case 'UserCountResponseDto':
|
||||
return UserCountResponseDto.fromJson(value);
|
||||
case 'UserDto':
|
||||
return UserDto.fromJson(value);
|
||||
case 'UserResponseDto':
|
||||
return UserResponseDto.fromJson(value);
|
||||
case 'ValidateAccessTokenResponseDto':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -97,6 +97,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is PathType) {
|
||||
return PathTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReactionType) {
|
||||
return ReactionTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SharedLinkType) {
|
||||
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user