Compare commits

...

33 Commits

Author SHA1 Message Date
Alex The Bot
cf4ec06750 Version v1.84.0 2023-11-01 14:46:59 +00:00
Alex
e8712e6694 fix(server): import scheduler module (#4766) 2023-10-31 23:40:35 -05:00
martin
ce5966c23d feat(web,server): activity (#4682)
* feat: activity

* regenerate api

* fix: make asset owner unable to delete comment

* fix: merge

* fix: tests

* feat: use textarea instead of input

* fix: do actions only if the album is shared

* fix: placeholder opacity

* fix(web): improve messages UI

* fix(web): improve input message UI

* pr feedback

* fix: tests

* pr feedback

* pr feedback

* pr feedback

* fix permissions

* regenerate api

* pr feedback

* pr feedback

* multiple improvements on web

* fix: ui colors

* WIP

* chore: open api

* pr feedback

* fix: add comment

* chore: clean up

* pr feedback

* refactor: endpoints

* chore: open api

* fix: filter by type

* fix: e2e

* feat: e2e remove own comment

* fix: web tests

* remove console.log

* chore: cleanup

* fix: ui tweaks

* pr feedback

* fix web test

* fix: unit tests

* chore: remove unused code

* revert useless changes

* fix: grouping messages

* fix: remove nullable on updatedAt

* fix: text overflow

* styling

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-31 22:13:34 -05:00
Jason Rasmussen
68f6446718 fix(cli): ignore web socket when unavailable and skip metadata init (#4748) 2023-10-31 22:08:21 -05:00
Jason Rasmussen
197f336b5f fix(web): no preload repair report (#4749) 2023-10-31 20:37:32 +00:00
Daniel Dietzler
cd375a976e feat(server): custom library scanning interval (#4390)
* add automatic library scan config options

* add validation

* open api

* use CronJob instead of cron-validator

* fix tests

* catch potential error of the library scan initialization

* better description for input field

* move library scan job initialization to server app service

* fix tests

* add comments to all parameters of cronjob contructor

* make scan a child of a more general library object

* open api

* chore: cleanup

* move cronjob handling to job repoistory

* web: select for common cron expressions

* fix open api

* fix tests

* put scanning settings in nested accordion

* fix system config validation

* refactor, tests

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-31 15:19:12 -05:00
Jason Rasmussen
088d5addf2 refactor(server): user core (#4733) 2023-10-31 11:01:32 -04:00
shenlong
2377df9dae fix(mobile): store exposure time as string (#4589)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-10-31 05:33:45 -05:00
waclaw66
ad5ba82f50 fix(mobile): don't show lens info if it's not available (#4737) 2023-10-31 05:33:08 -05:00
Michael Manganiello
b6f18cbe81 fix(server): Correctly set album start and end dates (#4698)
* fix(server): Correctly set album start and end dates

Currently, the query that retrieves album assets uses
`ORDER BY assets.fileCreatedAt DESC`, which makes the existing logic
return the start/end dates reversed (with `startDate` being taken from
the first asset in the array).

Instead of using the index-based approach, this change iterates through
assets to get the min/max `fileCreatedAt`. This will avoid any future
issues, if the query ordering changes, or becomes customizable (e.g. in
case the user prefers to visualize older assets first).

* fix: Maintain constant cost and only swap variables if needed
2023-10-31 05:08:34 -05:00
Mert
87a0ba3db3 feat(ml): export clip models to ONNX and host models on Hugging Face (#4700)
* export clip models

* export to hf

refactored export code

* export mclip, general refactoring

cleanup

* updated conda deps

* do transforms with pillow and numpy, add tokenization config to export, general refactoring

* moved conda dockerfile, re-added poetry

* minor fixes

* updated link

* updated tests

* removed `requirements.txt` from workflow

* fixed mimalloc path

* removed torchvision

* cleaner np typing

* review suggestions

* update default model name

* update test
2023-10-31 05:02:04 -05:00
Jason Rasmussen
3212a47720 refactor(server): user profile picture (#4728) 2023-10-30 19:38:34 -04:00
Jason Rasmussen
431536cdbb refactor(server): user core (#4722) 2023-10-30 17:02:36 -04:00
martin
9a60578088 fix(web): multiple improvements for people page (1) (#4717)
* fix(web): multiple improvements for people page

* feat: better responsive icons
2023-10-30 14:40:28 -05:00
Jason Rasmussen
8dcd159bd6 chore(server): remove user count endpoint (#4724)
* chore: remove unused endpoint

* chore: open api
2023-10-30 19:29:18 +00:00
Skyler Mäntysaari
2f87463170 fix(server): better fix for the OAuth Discovery errors (#4695)
* fix(server/oauth): Handle errors from OAuth Discovery.

* fix(server/oauth): Better fix for OAuth discovery error.

* This doesn't break tests.

* Update server/tsconfig.json

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* Revert back to the mostly original way.

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-30 13:22:30 -04:00
shenlong
9f56bf0ab9 refactor(mobile): app bar (#4687)
* refactor(mobile): add app bar to library and sharing

* mobile: add app bar dialog

* fix(mobile): refetch profile image only when path is changed

* mobile: add server url to dialog

* mobile: move trash to library app bar

* replace discord link with github

* user confirmation before sign out

* edit some styles

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-30 12:17:34 -05:00
Jason Rasmussen
603b056512 refactor(server): auth delete device (#4720)
* refactor(server): auth delete device

* fix: person e2e
2023-10-30 11:48:38 -04:00
Fynn Petersen-Frey
ce04e9e07a feat(server): hardware video acceleration for Rockchip SOCs via RKMPP (#4645)
* feat(server): hardware video acceleration for Rockchip SOCs via RKMPP

* add tests

* use LD_LIBRARY_PATH for custom ffmpeg

* incorporate review feedback

* code re-use for ffmpeg call

* review feedback
2023-10-30 09:39:37 -05:00
Alex
c54a188154 fix(web): sidebar setting not updating when there is a new property added to the data payload (#4708) 2023-10-30 09:17:37 -05:00
Mayuresh Dharwadkar
c77ba46d60 docs: fix typos (#4713) 2023-10-30 09:17:10 -05:00
martin
cc3149c520 fix(server): do not leak people (#4710) 2023-10-30 03:44:05 -05:00
shenlong
512f672e9e fix(mobile): cache key for assets from dto (#4699)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-10-29 15:28:54 -05:00
shenlong
b117985f66 fix(mobile): first char miss in new description (#4697)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-10-29 14:16:25 -05:00
Kalyani Mhala
b92a2b2a56 chore: add contribution section to readme (#4690)
* Update README.md

Successfully added contribution section to readme.md file.

* reordering

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-29 13:58:26 -05:00
Alex
a6f39bc74f fix(web): Improve UI/UX for shared link form (#4685)
* chore(web): Improve shared link form

* add verification for password

* improve ux
2023-10-29 13:50:43 -05:00
doggo
daad02504f feat(web): added toggle for Sharing button in the sidebar (#4674)
* Added toggle for Sharing button in the sidebar

* fix: format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-29 01:42:51 +00:00
jarvis2f
8a6889529c feat(server,web,mobile): Add optional password option for share links. (#4655)
* feat(server,web,mobile): Add optional password option for share links.

Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com>

* feat(server,web): Update shared-link.controller and page.svelte for improved cookie handling and metadata updates.

Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com>

---------

Signed-off-by: jarvis2f <137974272+jarvis2f@users.noreply.github.com>
2023-10-28 20:35:38 -05:00
Alex
b34cbd881a fix(web): scrollbar does not show all years (#4684) 2023-10-29 01:31:33 +00:00
martin
f6eaaab725 docs: update milestone page (#4683)
* docs: update milestone page

* docs: add 20k milestone
2023-10-28 20:20:05 -05:00
shenlong
2a2c74e081 fix(mobile): handle shared assets in viewer (#4679)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-10-28 14:48:30 -05:00
Skyler Mäntysaari
c653e0f261 fix(server/oauth): Handle errors from OAuth Discovery. (#4678) 2023-10-28 14:35:09 -05:00
martin
f0dd1d715a fix(web): table headers when there's no album (#4673) 2023-10-28 14:34:45 -05:00
247 changed files with 13595 additions and 3981 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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,435 @@ 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 {*} [options] Override http request option.
* @throws {RequiredError}
*/
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, 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;
}
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 {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getActivities(albumId: string, assetId?: string, type?: ReactionType, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, 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, 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
}
/**
* 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, 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 +14317,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 +14345,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 +14596,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 +14692,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 +14781,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 +14927,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 +16345,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 +16519,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 +16611,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 +16716,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 +16827,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.

View File

@@ -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).

View File

@@ -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).

View File

@@ -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).

View File

@@ -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
View 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

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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',

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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}")

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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()]

View File

@@ -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]]

View File

@@ -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()

View 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"]

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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"},
},
)

View 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"}},
)

View 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)

View 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"))

View 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()

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -1,2 +0,0 @@
# requirements to be installed with `--no-deps` flag
clip-server==0.8.*

View File

@@ -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')

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -265,6 +265,7 @@ class AlbumViewerPage extends HookConsumerWidget {
if (data.isRemote) buildControlButton(data),
],
),
isOwner: userId == data.ownerId,
),
),
),

View File

@@ -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(

View File

@@ -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()),

View File

@@ -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;

View File

@@ -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,
),
],
);

View File

@@ -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(),
],
);

View File

@@ -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,
),
@@ -774,7 +792,7 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(
tag: isFromResponse
tag: isFromDto
? '${a.remoteId}-$heroOffset'
: a.id + heroOffset,
),

View File

@@ -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(),

View File

@@ -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,
),
);
}

View File

@@ -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,
);
}

View File

@@ -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,
),
);
}

View File

@@ -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),
),
),
],
),
],
);
}
}

View File

@@ -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(),
],
),
);
}
}

View File

@@ -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,
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -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(),
);
}

View File

@@ -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,

View File

@@ -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 ^

View File

@@ -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,
),
);

View File

@@ -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,

View File

@@ -133,10 +133,7 @@ part 'router.gr.dart';
DuplicateGuard,
],
),
CustomRoute(
page: AppLogPage,
transitionsBuilder: TransitionsBuilders.slideBottom,
),
AutoRoute(page: AppLogPage, guards: [DuplicateGuard]),
AutoRoute(
page: AppLogDetailPage,
),

View File

@@ -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}';
}
}

View File

@@ -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;
}

View 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(),
],
),
),
),
);
}
}

View File

@@ -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,
),
),
],
),
),
);
}

View 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,
),
),
),
],
),
],
),
),
),
);
}
}

View 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(),
),
],
);
}
}

View File

@@ -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(

View File

@@ -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),
),
),

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

242
mobile/openapi/doc/ActivityApi.md generated Normal file
View File

@@ -0,0 +1,242 @@
# 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)
### 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 |
try {
final result = api_instance.getActivities(albumId, assetId, type);
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]
### 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
View 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)

View 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)

View 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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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) | |

View File

@@ -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)

View File

@@ -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** | |

View File

@@ -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) | |

View 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)

View 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)

View File

@@ -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
View 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)

View File

@@ -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';

227
mobile/openapi/lib/api/activity_api.dart generated Normal file
View File

@@ -0,0 +1,227 @@
//
// 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:
Future<Response> getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, }) 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));
}
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:
Future<List<ActivityResponseDto>?> getActivities(String albumId, { String? assetId, ReactionType? type, }) async {
final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, );
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;
}
}

View File

@@ -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));
}

View File

@@ -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:
///

View File

@@ -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':

View File

@@ -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();
}

View File

@@ -0,0 +1,140 @@
//
// 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 ActivityCreateDto {
/// Returns a new [ActivityCreateDto] instance.
ActivityCreateDto({
required this.albumId,
this.assetId,
this.comment,
required this.type,
});
String albumId;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? assetId;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? comment;
ReactionType type;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityCreateDto &&
other.albumId == albumId &&
other.assetId == assetId &&
other.comment == comment &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(albumId.hashCode) +
(assetId == null ? 0 : assetId!.hashCode) +
(comment == null ? 0 : comment!.hashCode) +
(type.hashCode);
@override
String toString() => 'ActivityCreateDto[albumId=$albumId, assetId=$assetId, comment=$comment, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'albumId'] = this.albumId;
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
if (this.comment != null) {
json[r'comment'] = this.comment;
} else {
// json[r'comment'] = null;
}
json[r'type'] = this.type;
return json;
}
/// Returns a new [ActivityCreateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityCreateDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityCreateDto(
albumId: mapValueOfType<String>(json, r'albumId')!,
assetId: mapValueOfType<String>(json, r'assetId'),
comment: mapValueOfType<String>(json, r'comment'),
type: ReactionType.fromJson(json[r'type'])!,
);
}
return null;
}
static List<ActivityCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityCreateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityCreateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityCreateDto> mapFromJson(dynamic json) {
final map = <String, ActivityCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityCreateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityCreateDto-objects as value to a dart map
static Map<String, List<ActivityCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityCreateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'albumId',
'type',
};
}

View File

@@ -0,0 +1,219 @@
//
// 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 ActivityResponseDto {
/// Returns a new [ActivityResponseDto] instance.
ActivityResponseDto({
required this.assetId,
this.comment,
required this.createdAt,
required this.id,
required this.type,
required this.user,
});
String? assetId;
String? comment;
DateTime createdAt;
String id;
ActivityResponseDtoTypeEnum type;
UserDto user;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityResponseDto &&
other.assetId == assetId &&
other.comment == comment &&
other.createdAt == createdAt &&
other.id == id &&
other.type == type &&
other.user == user;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetId == null ? 0 : assetId!.hashCode) +
(comment == null ? 0 : comment!.hashCode) +
(createdAt.hashCode) +
(id.hashCode) +
(type.hashCode) +
(user.hashCode);
@override
String toString() => 'ActivityResponseDto[assetId=$assetId, comment=$comment, createdAt=$createdAt, id=$id, type=$type, user=$user]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
if (this.comment != null) {
json[r'comment'] = this.comment;
} else {
// json[r'comment'] = null;
}
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id;
json[r'type'] = this.type;
json[r'user'] = this.user;
return json;
}
/// Returns a new [ActivityResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityResponseDto(
assetId: mapValueOfType<String>(json, r'assetId'),
comment: mapValueOfType<String>(json, r'comment'),
createdAt: mapDateTime(json, r'createdAt', '')!,
id: mapValueOfType<String>(json, r'id')!,
type: ActivityResponseDtoTypeEnum.fromJson(json[r'type'])!,
user: UserDto.fromJson(json[r'user'])!,
);
}
return null;
}
static List<ActivityResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityResponseDto> mapFromJson(dynamic json) {
final map = <String, ActivityResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityResponseDto-objects as value to a dart map
static Map<String, List<ActivityResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetId',
'createdAt',
'id',
'type',
'user',
};
}
class ActivityResponseDtoTypeEnum {
/// Instantiate a new enum with the provided [value].
const ActivityResponseDtoTypeEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const comment = ActivityResponseDtoTypeEnum._(r'comment');
static const like = ActivityResponseDtoTypeEnum._(r'like');
/// List of all possible values in this [enum][ActivityResponseDtoTypeEnum].
static const values = <ActivityResponseDtoTypeEnum>[
comment,
like,
];
static ActivityResponseDtoTypeEnum? fromJson(dynamic value) => ActivityResponseDtoTypeEnumTypeTransformer().decode(value);
static List<ActivityResponseDtoTypeEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityResponseDtoTypeEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityResponseDtoTypeEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [ActivityResponseDtoTypeEnum] to String,
/// and [decode] dynamic data back to [ActivityResponseDtoTypeEnum].
class ActivityResponseDtoTypeEnumTypeTransformer {
factory ActivityResponseDtoTypeEnumTypeTransformer() => _instance ??= const ActivityResponseDtoTypeEnumTypeTransformer._();
const ActivityResponseDtoTypeEnumTypeTransformer._();
String encode(ActivityResponseDtoTypeEnum data) => data.value;
/// Decodes a [dynamic value][data] to a ActivityResponseDtoTypeEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
ActivityResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'comment': return ActivityResponseDtoTypeEnum.comment;
case r'like': return ActivityResponseDtoTypeEnum.like;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [ActivityResponseDtoTypeEnumTypeTransformer] instance.
static ActivityResponseDtoTypeEnumTypeTransformer? _instance;
}

View File

@@ -0,0 +1,98 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityStatisticsResponseDto {
/// Returns a new [ActivityStatisticsResponseDto] instance.
ActivityStatisticsResponseDto({
required this.comments,
});
int comments;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityStatisticsResponseDto &&
other.comments == comments;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(comments.hashCode);
@override
String toString() => 'ActivityStatisticsResponseDto[comments=$comments]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'comments'] = this.comments;
return json;
}
/// Returns a new [ActivityStatisticsResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityStatisticsResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityStatisticsResponseDto(
comments: mapValueOfType<int>(json, r'comments')!,
);
}
return null;
}
static List<ActivityStatisticsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityStatisticsResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityStatisticsResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityStatisticsResponseDto> mapFromJson(dynamic json) {
final map = <String, ActivityStatisticsResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityStatisticsResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityStatisticsResponseDto-objects as value to a dart map
static Map<String, List<ActivityStatisticsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityStatisticsResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityStatisticsResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'comments',
};
}

View File

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

View File

@@ -19,6 +19,7 @@ class SharedLinkCreateDto {
this.assetIds = const [],
this.description,
this.expiresAt,
this.password,
this.showMetadata = true,
required this.type,
});
@@ -47,6 +48,14 @@ class SharedLinkCreateDto {
DateTime? expiresAt;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? password;
bool showMetadata;
SharedLinkType type;
@@ -59,6 +68,7 @@ class SharedLinkCreateDto {
other.assetIds == assetIds &&
other.description == description &&
other.expiresAt == expiresAt &&
other.password == password &&
other.showMetadata == showMetadata &&
other.type == type;
@@ -71,11 +81,12 @@ class SharedLinkCreateDto {
(assetIds.hashCode) +
(description == null ? 0 : description!.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(password == null ? 0 : password!.hashCode) +
(showMetadata.hashCode) +
(type.hashCode);
@override
String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata, type=$type]';
String toString() => 'SharedLinkCreateDto[albumId=$albumId, allowDownload=$allowDownload, allowUpload=$allowUpload, assetIds=$assetIds, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -96,6 +107,11 @@ class SharedLinkCreateDto {
json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String();
} else {
// json[r'expiresAt'] = null;
}
if (this.password != null) {
json[r'password'] = this.password;
} else {
// json[r'password'] = null;
}
json[r'showMetadata'] = this.showMetadata;
json[r'type'] = this.type;
@@ -118,6 +134,7 @@ class SharedLinkCreateDto {
: const [],
description: mapValueOfType<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''),
password: mapValueOfType<String>(json, r'password'),
showMetadata: mapValueOfType<bool>(json, r'showMetadata') ?? true,
type: SharedLinkType.fromJson(json[r'type'])!,
);

View File

@@ -18,6 +18,7 @@ class SharedLinkEditDto {
this.changeExpiryTime,
this.description,
this.expiresAt,
this.password,
this.showMetadata,
});
@@ -56,6 +57,14 @@ class SharedLinkEditDto {
DateTime? expiresAt;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? password;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -71,6 +80,7 @@ class SharedLinkEditDto {
other.changeExpiryTime == changeExpiryTime &&
other.description == description &&
other.expiresAt == expiresAt &&
other.password == password &&
other.showMetadata == showMetadata;
@override
@@ -81,10 +91,11 @@ class SharedLinkEditDto {
(changeExpiryTime == null ? 0 : changeExpiryTime!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(password == null ? 0 : password!.hashCode) +
(showMetadata == null ? 0 : showMetadata!.hashCode);
@override
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, showMetadata=$showMetadata]';
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -113,6 +124,11 @@ class SharedLinkEditDto {
} else {
// json[r'expiresAt'] = null;
}
if (this.password != null) {
json[r'password'] = this.password;
} else {
// json[r'password'] = null;
}
if (this.showMetadata != null) {
json[r'showMetadata'] = this.showMetadata;
} else {
@@ -134,6 +150,7 @@ class SharedLinkEditDto {
changeExpiryTime: mapValueOfType<bool>(json, r'changeExpiryTime'),
description: mapValueOfType<String>(json, r'description'),
expiresAt: mapDateTime(json, r'expiresAt', ''),
password: mapValueOfType<String>(json, r'password'),
showMetadata: mapValueOfType<bool>(json, r'showMetadata'),
);
}

View File

@@ -22,7 +22,9 @@ class SharedLinkResponseDto {
required this.expiresAt,
required this.id,
required this.key,
required this.password,
required this.showMetadata,
this.token,
required this.type,
required this.userId,
});
@@ -51,8 +53,12 @@ class SharedLinkResponseDto {
String key;
String? password;
bool showMetadata;
String? token;
SharedLinkType type;
String userId;
@@ -68,7 +74,9 @@ class SharedLinkResponseDto {
other.expiresAt == expiresAt &&
other.id == id &&
other.key == key &&
other.password == password &&
other.showMetadata == showMetadata &&
other.token == token &&
other.type == type &&
other.userId == userId;
@@ -84,12 +92,14 @@ class SharedLinkResponseDto {
(expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) +
(key.hashCode) +
(password == null ? 0 : password!.hashCode) +
(showMetadata.hashCode) +
(token == null ? 0 : token!.hashCode) +
(type.hashCode) +
(userId.hashCode);
@override
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, showMetadata=$showMetadata, type=$type, userId=$userId]';
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -114,7 +124,17 @@ class SharedLinkResponseDto {
}
json[r'id'] = this.id;
json[r'key'] = this.key;
if (this.password != null) {
json[r'password'] = this.password;
} else {
// json[r'password'] = null;
}
json[r'showMetadata'] = this.showMetadata;
if (this.token != null) {
json[r'token'] = this.token;
} else {
// json[r'token'] = null;
}
json[r'type'] = this.type;
json[r'userId'] = this.userId;
return json;
@@ -137,7 +157,9 @@ class SharedLinkResponseDto {
expiresAt: mapDateTime(json, r'expiresAt', ''),
id: mapValueOfType<String>(json, r'id')!,
key: mapValueOfType<String>(json, r'key')!,
password: mapValueOfType<String>(json, r'password'),
showMetadata: mapValueOfType<bool>(json, r'showMetadata')!,
token: mapValueOfType<String>(json, r'token'),
type: SharedLinkType.fromJson(json[r'type'])!,
userId: mapValueOfType<String>(json, r'userId')!,
);
@@ -195,6 +217,7 @@ class SharedLinkResponseDto {
'expiresAt',
'id',
'key',
'password',
'showMetadata',
'type',
'userId',

View File

@@ -15,6 +15,7 @@ class SystemConfigDto {
SystemConfigDto({
required this.ffmpeg,
required this.job,
required this.library_,
required this.machineLearning,
required this.map,
required this.newVersionCheck,
@@ -31,6 +32,8 @@ class SystemConfigDto {
SystemConfigJobDto job;
SystemConfigLibraryDto library_;
SystemConfigMachineLearningDto machineLearning;
SystemConfigMapDto map;
@@ -55,6 +58,7 @@ class SystemConfigDto {
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
other.ffmpeg == ffmpeg &&
other.job == job &&
other.library_ == library_ &&
other.machineLearning == machineLearning &&
other.map == map &&
other.newVersionCheck == newVersionCheck &&
@@ -71,6 +75,7 @@ class SystemConfigDto {
// ignore: unnecessary_parenthesis
(ffmpeg.hashCode) +
(job.hashCode) +
(library_.hashCode) +
(machineLearning.hashCode) +
(map.hashCode) +
(newVersionCheck.hashCode) +
@@ -83,12 +88,13 @@ class SystemConfigDto {
(trash.hashCode);
@override
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]';
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'ffmpeg'] = this.ffmpeg;
json[r'job'] = this.job;
json[r'library'] = this.library_;
json[r'machineLearning'] = this.machineLearning;
json[r'map'] = this.map;
json[r'newVersionCheck'] = this.newVersionCheck;
@@ -112,6 +118,7 @@ class SystemConfigDto {
return SystemConfigDto(
ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
job: SystemConfigJobDto.fromJson(json[r'job'])!,
library_: SystemConfigLibraryDto.fromJson(json[r'library'])!,
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
map: SystemConfigMapDto.fromJson(json[r'map'])!,
newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!,
@@ -171,6 +178,7 @@ class SystemConfigDto {
static const requiredKeys = <String>{
'ffmpeg',
'job',
'library',
'machineLearning',
'map',
'newVersionCheck',

View File

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

View File

@@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SystemConfigLibraryScanDto {
/// Returns a new [SystemConfigLibraryScanDto] instance.
SystemConfigLibraryScanDto({
required this.cronExpression,
required this.enabled,
});
String cronExpression;
bool enabled;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryScanDto &&
other.cronExpression == cronExpression &&
other.enabled == enabled;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(cronExpression.hashCode) +
(enabled.hashCode);
@override
String toString() => 'SystemConfigLibraryScanDto[cronExpression=$cronExpression, enabled=$enabled]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'cronExpression'] = this.cronExpression;
json[r'enabled'] = this.enabled;
return json;
}
/// Returns a new [SystemConfigLibraryScanDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigLibraryScanDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SystemConfigLibraryScanDto(
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
enabled: mapValueOfType<bool>(json, r'enabled')!,
);
}
return null;
}
static List<SystemConfigLibraryScanDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigLibraryScanDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigLibraryScanDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigLibraryScanDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigLibraryScanDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigLibraryScanDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigLibraryScanDto-objects as value to a dart map
static Map<String, List<SystemConfigLibraryScanDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigLibraryScanDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SystemConfigLibraryScanDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'cronExpression',
'enabled',
};
}

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