Compare commits

...

41 Commits

Author SHA1 Message Date
Alex Tran
b49d9660f6 reduce widget rebuild 2023-11-02 21:33:33 -05:00
Jason Rasmussen
b58edae134 fix(web): timeline alignment (#4808) 2023-11-02 15:11:59 -05:00
martin
2b9f20a1b5 fix: update like status (#4803) 2023-11-02 14:43:27 -04:00
Alex
d5f8199655 fix(web): scrollbar not showing year (#4782)
* fix(web): scrollbar not showing year

* grammar

* fix test
2023-11-01 20:50:24 -05:00
Alex
d8903de92e docs: remove read-only related content (#4781)
* docs: remove read-only related content

* format

* broken link
2023-11-01 20:49:57 -05:00
Jason Rasmussen
1d35965d03 feat(web): shuffle slideshow order (#4277)
* feat(web): shuffle slideshow order

* Fix play/stop issues

* Enter/exit fullscreen mode
* Prevent navigation to the next asset after exiting slideshow mode

* Fix entering the slideshow mode from an album page

* Simplify markup of the AssetViewer

Group viewer area and navigation (prev/next/slideshow bar) controls together

* Select a random asset from a random bucket

* Preserve assets order in random mode

* Exit fullscreen mode only if it is active

* Extract SlideshowHistory class

* Use traditional functions instead of arrow functions

* Refactor SlideshowHistory class

* Extract SlideshowBar component

* Fix comments

* Hide Say something in slideshow mode

---------

Co-authored-by: brighteyed <sergey.kondrikov@gmail.com>
2023-11-01 21:34:30 -04:00
Alex
309bf1ad22 chore: post release tasks 2023-11-01 14:43:10 -05:00
Jason Rasmussen
0130591a0f fix: show/set activity like per user (#4775)
* fix: like per user

* chore: open api

* chore: e2e test for userId filtering
2023-11-01 11:49:12 -04:00
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
264 changed files with 14303 additions and 4308 deletions

View File

@@ -166,7 +166,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install --with dev poetry install --with dev
poetry run pip install --no-deps -r requirements.txt
- name: Lint with ruff - name: Lint with ruff
run: | run: |
poetry run ruff check --format=github app 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 Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
``` ```
# Features ## Features
| Features | Mobile | Web | | 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 | | Offline support | Yes | No |
| Read-only gallery | Yes | Yes | | 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. 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. 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 - [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 - [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - 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
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -99,6 +99,103 @@ export interface APIKeyUpdateDto {
*/ */
'name': string; '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 * @export
@@ -2490,6 +2587,20 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const ReactionType = {
Comment: 'comment',
Like: 'like'
} as const;
export type ReactionType = typeof ReactionType[keyof typeof ReactionType];
/** /**
* *
* @export * @export
@@ -3038,6 +3149,12 @@ export interface SharedLinkCreateDto {
* @memberof SharedLinkCreateDto * @memberof SharedLinkCreateDto
*/ */
'expiresAt'?: string | null; 'expiresAt'?: string | null;
/**
*
* @type {string}
* @memberof SharedLinkCreateDto
*/
'password'?: string;
/** /**
* *
* @type {boolean} * @type {boolean}
@@ -3089,6 +3206,12 @@ export interface SharedLinkEditDto {
* @memberof SharedLinkEditDto * @memberof SharedLinkEditDto
*/ */
'expiresAt'?: string | null; 'expiresAt'?: string | null;
/**
*
* @type {string}
* @memberof SharedLinkEditDto
*/
'password'?: string;
/** /**
* *
* @type {boolean} * @type {boolean}
@@ -3156,12 +3279,24 @@ export interface SharedLinkResponseDto {
* @memberof SharedLinkResponseDto * @memberof SharedLinkResponseDto
*/ */
'key': string; 'key': string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'password': string | null;
/** /**
* *
* @type {boolean} * @type {boolean}
* @memberof SharedLinkResponseDto * @memberof SharedLinkResponseDto
*/ */
'showMetadata': boolean; 'showMetadata': boolean;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'token'?: string | null;
/** /**
* *
* @type {SharedLinkType} * @type {SharedLinkType}
@@ -3259,6 +3394,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto * @memberof SystemConfigDto
*/ */
'job': SystemConfigJobDto; 'job': SystemConfigJobDto;
/**
*
* @type {SystemConfigLibraryDto}
* @memberof SystemConfigDto
*/
'library': SystemConfigLibraryDto;
/** /**
* *
* @type {SystemConfigMachineLearningDto} * @type {SystemConfigMachineLearningDto}
@@ -3510,6 +3651,38 @@ export interface SystemConfigJobDto {
*/ */
'videoConversion': JobSettingsDto; '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 * @export
@@ -3940,6 +4113,7 @@ export const TranscodeHWAccel = {
Nvenc: 'nvenc', Nvenc: 'nvenc',
Qsv: 'qsv', Qsv: 'qsv',
Vaapi: 'vaapi', Vaapi: 'vaapi',
Rkmpp: 'rkmpp',
Disabled: 'disabled' Disabled: 'disabled'
} as const; } as const;
@@ -4188,15 +4362,39 @@ export interface UsageByUserDto {
/** /**
* *
* @export * @export
* @interface UserCountResponseDto * @interface UserDto
*/ */
export interface UserCountResponseDto { export interface UserDto {
/** /**
* *
* @type {number} * @type {string}
* @memberof UserCountResponseDto * @memberof UserDto
*/ */
'userCount': number; 'email': string;
/**
*
* @type {string}
* @memberof UserDto
*/
'firstName': string;
/**
*
* @type {string}
* @memberof UserDto
*/
'id': string;
/**
*
* @type {string}
* @memberof UserDto
*/
'lastName': string;
/**
*
* @type {string}
* @memberof UserDto
*/
'profileImagePath': string;
} }
/** /**
* *
@@ -4781,6 +4979,448 @@ export class APIKeyApi extends BaseAPI {
} }
/**
* ActivityApi - axios parameter creator
* @export
*/
export const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {ActivityCreateDto} activityCreateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createActivity: async (activityCreateDto: ActivityCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'activityCreateDto' is not null or undefined
assertParamExists('createActivity', 'activityCreateDto', activityCreateDto)
const localVarPath = `/activity`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(activityCreateDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteActivity: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteActivity', 'id', id)
const localVarPath = `/activity/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} albumId
* @param {string} [assetId]
* @param {ReactionType} [type]
* @param {string} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'albumId' is not null or undefined
assertParamExists('getActivities', 'albumId', albumId)
const localVarPath = `/activity`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (albumId !== undefined) {
localVarQueryParameter['albumId'] = albumId;
}
if (assetId !== undefined) {
localVarQueryParameter['assetId'] = assetId;
}
if (type !== undefined) {
localVarQueryParameter['type'] = type;
}
if (userId !== undefined) {
localVarQueryParameter['userId'] = userId;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} albumId
* @param {string} [assetId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getActivityStatistics: async (albumId: string, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'albumId' is not null or undefined
assertParamExists('getActivityStatistics', 'albumId', albumId)
const localVarPath = `/activity/statistics`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (albumId !== undefined) {
localVarQueryParameter['albumId'] = albumId;
}
if (assetId !== undefined) {
localVarQueryParameter['assetId'] = assetId;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* ActivityApi - functional programming interface
* @export
*/
export const ActivityApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)
return {
/**
*
* @param {ActivityCreateDto} activityCreateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createActivity(activityCreateDto: ActivityCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(activityCreateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteActivity(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteActivity(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} albumId
* @param {string} [assetId]
* @param {ReactionType} [type]
* @param {string} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} albumId
* @param {string} [assetId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getActivityStatistics(albumId: string, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityStatisticsResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivityStatistics(albumId, assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* ActivityApi - factory interface
* @export
*/
export const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ActivityApiFp(configuration)
return {
/**
*
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityResponseDto> {
return localVarFp.createActivity(requestParameters.activityCreateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.deleteActivity(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityStatisticsResponseDto> {
return localVarFp.getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for createActivity operation in ActivityApi.
* @export
* @interface ActivityApiCreateActivityRequest
*/
export interface ActivityApiCreateActivityRequest {
/**
*
* @type {ActivityCreateDto}
* @memberof ActivityApiCreateActivity
*/
readonly activityCreateDto: ActivityCreateDto
}
/**
* Request parameters for deleteActivity operation in ActivityApi.
* @export
* @interface ActivityApiDeleteActivityRequest
*/
export interface ActivityApiDeleteActivityRequest {
/**
*
* @type {string}
* @memberof ActivityApiDeleteActivity
*/
readonly id: string
}
/**
* Request parameters for getActivities operation in ActivityApi.
* @export
* @interface ActivityApiGetActivitiesRequest
*/
export interface ActivityApiGetActivitiesRequest {
/**
*
* @type {string}
* @memberof ActivityApiGetActivities
*/
readonly albumId: string
/**
*
* @type {string}
* @memberof ActivityApiGetActivities
*/
readonly assetId?: string
/**
*
* @type {ReactionType}
* @memberof ActivityApiGetActivities
*/
readonly type?: ReactionType
/**
*
* @type {string}
* @memberof ActivityApiGetActivities
*/
readonly userId?: string
}
/**
* Request parameters for getActivityStatistics operation in ActivityApi.
* @export
* @interface ActivityApiGetActivityStatisticsRequest
*/
export interface ActivityApiGetActivityStatisticsRequest {
/**
*
* @type {string}
* @memberof ActivityApiGetActivityStatistics
*/
readonly albumId: string
/**
*
* @type {string}
* @memberof ActivityApiGetActivityStatistics
*/
readonly assetId?: string
}
/**
* ActivityApi - object-oriented interface
* @export
* @class ActivityApi
* @extends {BaseAPI}
*/
export class ActivityApi extends BaseAPI {
/**
*
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ActivityApi
*/
public createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).createActivity(requestParameters.activityCreateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ActivityApi
*/
public deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).deleteActivity(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ActivityApi
*/
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ActivityApi
*/
public getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
}
}
/** /**
* AlbumApi - axios parameter creator * AlbumApi - axios parameter creator
* @export * @export
@@ -13690,11 +14330,13 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
}, },
/** /**
* *
* @param {string} [password]
* @param {string} [token]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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`; const localVarPath = `/shared-link/me`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -13716,6 +14358,14 @@ export const SharedLinkApiAxiosParamCreator = function (configuration?: Configur
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration) await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (password !== undefined) {
localVarQueryParameter['password'] = password;
}
if (token !== undefined) {
localVarQueryParameter['token'] = token;
}
if (key !== undefined) { if (key !== undefined) {
localVarQueryParameter['key'] = key; localVarQueryParameter['key'] = key;
} }
@@ -13959,12 +14609,14 @@ export const SharedLinkApiFp = function(configuration?: Configuration) {
}, },
/** /**
* *
* @param {string} [password]
* @param {string} [token]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getMySharedLink(key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> { async getMySharedLink(password?: string, token?: string, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(password, token, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@@ -14053,7 +14705,7 @@ export const SharedLinkApiFactory = function (configuration?: Configuration, bas
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> { getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.getMySharedLink(requestParameters.key, options).then((request) => request(axios, basePath)); return localVarFp.getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@@ -14142,6 +14794,20 @@ export interface SharedLinkApiCreateSharedLinkRequest {
* @interface SharedLinkApiGetMySharedLinkRequest * @interface SharedLinkApiGetMySharedLinkRequest
*/ */
export interface SharedLinkApiGetMySharedLinkRequest { export interface SharedLinkApiGetMySharedLinkRequest {
/**
*
* @type {string}
* @memberof SharedLinkApiGetMySharedLink
*/
readonly password?: string
/**
*
* @type {string}
* @memberof SharedLinkApiGetMySharedLink
*/
readonly token?: string
/** /**
* *
* @type {string} * @type {string}
@@ -14274,7 +14940,7 @@ export class SharedLinkApi extends BaseAPI {
* @memberof SharedLinkApi * @memberof SharedLinkApi
*/ */
public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) { public getMySharedLink(requestParameters: SharedLinkApiGetMySharedLinkRequest = {}, options?: AxiosRequestConfig) {
return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.key, options).then((request) => request(this.axios, this.basePath)); return SharedLinkApiFp(this.configuration).getMySharedLink(requestParameters.password, requestParameters.token, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@@ -15692,49 +16358,6 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {boolean} [admin]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getUserCount: async (admin?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/user/count`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (admin !== undefined) {
localVarQueryParameter['admin'] = admin;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -15909,16 +16532,6 @@ export const UserApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserById(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getUserById(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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 * @param {string} id
@@ -16011,15 +16624,6 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
getUserById(requestParameters: UserApiGetUserByIdRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> { getUserById(requestParameters: UserApiGetUserByIdRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
return localVarFp.getUserById(requestParameters.id, options).then((request) => request(axios, basePath)); 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. * @param {UserApiRestoreUserRequest} requestParameters Request parameters.
@@ -16125,20 +16729,6 @@ export interface UserApiGetUserByIdRequest {
readonly id: string 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. * Request parameters for restoreUser operation in UserApi.
* @export * @export
@@ -16250,17 +16840,6 @@ export class UserApi extends BaseAPI {
return UserApiFp(this.configuration).getUserById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); 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. * @param {UserApiRestoreUserRequest} requestParameters Request parameters.

View File

@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * 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

@@ -33,8 +33,6 @@ To be concise, Immich can now read in the gallery files, register the path into
- Only new files that are added to the gallery will be detected. - Only new files that are added to the gallery will be detected.
- Deleted and moved files will not be detected. - Deleted and moved files will not be detected.
You can find more information on how to use the feature by reading the documentation [here](/docs/features/read-only-gallery).
## Memory feature ## Memory feature
This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features. This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features.

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

@@ -32,7 +32,6 @@ immich
| --server / -s | Immich's server address | | --server / -s | Immich's server address |
| --threads / -t | Number of threads to use (Default 5) | | --threads / -t | Number of threads to use (Default 5) |
| --album/ -al | Create albums for assets based on the parent folder or a given name | | --album/ -al | Create albums for assets based on the parent folder or a given name |
| --import/ -i | Import gallery (assets are not uploaded) |
## Quick Start ## Quick Start
@@ -108,70 +107,3 @@ npm run build
```bash title="Run the command" ```bash title="Run the command"
node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory
``` ```
---
## Importing existing libraries
If you do not wish to upload files into the server, existing files can be imported into the immich gallery through the use of the `--import` flag.
```
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/ --import
```
```
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg --import
```
The `immich-server` and `immich-microservices` containers must be able to access the files, or directories at the path referenced in the command. The directories referenced must be set under a user's `External Path` setting. More detailed instructions can be found [here](/docs/features/read-only-gallery).
:::tip Matching volume references
The import command is most easily run on the machine running the immich service, as the path to the files on the machine running the command and the server much match identically.
If you are running immich within docker, the volume pointing to your existing library should be identical with your host machine.
```diff title="docker-compose.yml"
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /path/to/media:/path/to/media
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "microservices" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /path/to/media:/path/to/media
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
```
The proper command for above would be as shown below. You should have access to `/path/to/media` exactly on the environment the CLI command is being run on
```
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
```
If you are running the import using the docker command, please note that the volumes should point to the `/path/to/media` exactly on the environment the CLI command is being run on
```
docker run -it --rm -v "/path/to/media:/path/to/media" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
```
:::

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: 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/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. - `/mnt/media/videos`: Videos from the same christmas trip.

View File

@@ -1,97 +0,0 @@
# Read-only Gallery [Deprecated]
:::caution
This feature is being deprecated in favor of [Libraries](/docs/features/libraries.md).
:::
## Overview
This feature enables users to use an existing gallery without uploading the assets to Immich.
Upon syncing the file information, it will be read by Immich to generate supported files.
## Usage
:::tip Example scenario
On the VM/system that Immich is running, I have 2 galleries that I want to use with Immich.
- My gallery is stored at `/mnt/media/precious-memory`
- My wife's gallery is stored at `/mnt/media/childhood-memory`
We will use those values in the steps below.
:::
### Mount the gallery to the containers.
`immich-server` and `immich-microservices` containers will need access to the gallery. Mount the directory path as in the example below
```diff title="docker-compose.yml"
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "microservices" ]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
env_file:
- .env
depends_on:
- redis
- database
- typesense
restart: always
```
:::tip
Internal and external path have to be identical.
:::
_Remember to bring the container down/up to register the changes. Make sure you can see the mounted path in the container._
### Register the path for the user.
This action is done by the admin of the instance.
- Navigate to `Administration > Users` page on the web.
- Click on the user edit button.
- Add the gallery path to the `External Path` field for the corresponding user and confirm the changes.
<img src={require('./img/me.png').default} width='33%' title='My Account Storage Path' />
<img src={require('./img/my-wife.png').default} width='33%' title='My Wifes Account Storage Path' />
### Sync with the CLI tool.
- Install or update the [CLI Tool](/docs/features/bulk-upload.md). The import feature is supported from version `v0.39.0` of the CLI
- Run the command below to sync the gallery with Immich.
```bash title="Import my gallery"
immich upload --key <my-api-key> --server http://my-server-ip:2283/api /mnt/media/precious-memory --recursive --import
```
```bash title="Import my wife gallery"
immich upload --key <my-wife-api-key> --server http://my-server-ip:2283/api /mnt/media/childhood-memory --recursive --import
```
The `--import` flag will tell Immich to import the files by path instead of uploading them.

View File

@@ -8,6 +8,7 @@ import {
mdiCheckAll, mdiCheckAll,
mdiCheckboxMarked, mdiCheckboxMarked,
mdiCollage, mdiCollage,
mdiContentCopy,
mdiDevices, mdiDevices,
mdiFaceMan, mdiFaceMan,
mdiFaceManOutline, mdiFaceManOutline,
@@ -26,6 +27,7 @@ import {
mdiMerge, mdiMerge,
mdiMonitor, mdiMonitor,
mdiMotionPlayOutline, mdiMotionPlayOutline,
mdiPalette,
mdiPanVertical, mdiPanVertical,
mdiPartyPopper, mdiPartyPopper,
mdiRaw, mdiRaw,
@@ -47,6 +49,33 @@ import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline'; import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [ 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, icon: mdiTrashCanOutline,
title: 'Trash Feature', title: 'Trash Feature',
@@ -283,7 +312,7 @@ const items: Item[] = [
}, },
{ {
icon: mdiStar, icon: mdiStar,
description: 'Reach 10K Starts on GitHub!', description: 'Reach 10K Stars on GitHub!',
title: '10,000 Stars', title: '10,000 Stars',
release: 'v1.54.0', release: 'v1.54.0',
tag: '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 RUN python -m venv /opt/venv
ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}" 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 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 FROM python:3.11-slim-bookworm

View File

@@ -1,5 +1,6 @@
import json import json
from typing import Any, Iterator, TypeAlias from pathlib import Path
from typing import Any, Iterator
from unittest import mock from unittest import mock
import numpy as np import numpy as np
@@ -8,8 +9,7 @@ from fastapi.testclient import TestClient
from PIL import Image from PIL import Image
from .main import app, init_state from .main import app, init_state
from .schemas import ndarray_f32
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
@pytest.fixture @pytest.fixture
@@ -18,13 +18,13 @@ def pil_image() -> Image.Image:
@pytest.fixture @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 return np.asarray(pil_image)[:, :, ::-1] # PIL uses RGB while cv2 uses BGR
@pytest.fixture @pytest.fixture
def mock_get_model() -> Iterator[mock.Mock]: 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 yield mocked
@@ -37,3 +37,25 @@ def deployed_app() -> TestClient:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def responses() -> dict[str, Any]: def responses() -> dict[str, Any]:
return json.load(open("responses.json", "r")) 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 .facial_recognition import FaceRecognizer
from .image_classification import ImageClassifier 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: ) -> None:
self.model_name = model_name self.model_name = model_name
self.loaded = False 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"]) self.providers = model_kwargs.pop("providers", ["CPUExecutionProvider"])
# don't pre-allocate more memory than needed # don't pre-allocate more memory than needed
self.provider_options = model_kwargs.pop( self.provider_options = model_kwargs.pop(
@@ -92,7 +92,7 @@ class InferenceModel(ABC):
@property @property
def cache_dir(self) -> Path: 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 @cache_dir.setter
def cache_dir(self, cache_dir: Path) -> None: 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.lock import OptimisticLock
from aiocache.plugins import BasePlugin, TimingPlugin from aiocache.plugins import BasePlugin, TimingPlugin
from app.models import from_model_type
from ..schemas import ModelType from ..schemas import ModelType
from .base import InferenceModel from .base import InferenceModel
@@ -50,7 +52,7 @@ class ModelCache:
async with OptimisticLock(self.cache, key) as lock: async with OptimisticLock(self.cache, key) as lock:
model = await self.cache.get(key) model = await self.cache.get(key)
if model is None: 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) await lock.cas(model, ttl=self.ttl)
return model return model

View File

@@ -1,23 +1,24 @@
import os import json
import zipfile from abc import abstractmethod
from functools import cached_property
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
import numpy as np
import onnxruntime as ort import onnxruntime as ort
import torch from huggingface_hub import snapshot_download
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 PIL import Image 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 from .base import InferenceModel
class CLIPEncoder(InferenceModel): class BaseCLIPEncoder(InferenceModel):
_model_type = ModelType.CLIP _model_type = ModelType.CLIP
def __init__( def __init__(
@@ -27,48 +28,29 @@ class CLIPEncoder(InferenceModel):
mode: Literal["text", "vision"] | None = None, mode: Literal["text", "vision"] | None = None,
**model_kwargs: Any, **model_kwargs: Any,
) -> None: ) -> 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 self.mode = mode
super().__init__(model_name, cache_dir, **model_kwargs) super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self) -> None:
models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
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: def _load(self) -> None:
if self.mode == "text" or self.mode is None: if self.mode == "text" or self.mode is None:
log.debug(f"Loading clip text model '{self.model_name}'") log.debug(f"Loading clip text model '{self.model_name}'")
self.text_model = ort.InferenceSession( self.text_model = ort.InferenceSession(
self.cache_dir / "textual.onnx", self.textual_path.as_posix(),
sess_options=self.sess_options, sess_options=self.sess_options,
providers=self.providers, providers=self.providers,
provider_options=self.provider_options, 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: if self.mode == "vision" or self.mode is None:
log.debug(f"Loading clip vision model '{self.model_name}'") log.debug(f"Loading clip vision model '{self.model_name}'")
self.vision_model = ort.InferenceSession( self.vision_model = ort.InferenceSession(
self.cache_dir / "visual.onnx", self.visual_path.as_posix(),
sess_options=self.sess_options, sess_options=self.sess_options,
providers=self.providers, providers=self.providers,
provider_options=self.provider_options, 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]: def _predict(self, image_or_text: Image.Image | str) -> list[float]:
if isinstance(image_or_text, bytes): if isinstance(image_or_text, bytes):
@@ -78,55 +60,163 @@ class CLIPEncoder(InferenceModel):
case Image.Image(): case Image.Image():
if self.mode == "text": if self.mode == "text":
raise TypeError("Cannot encode image as text-only model") raise TypeError("Cannot encode image as text-only model")
pixel_values = self.transform(image_or_text)
assert isinstance(pixel_values, torch.Tensor) outputs = self.vision_model.run(None, self.transform(image_or_text))
pixel_values = torch.unsqueeze(pixel_values, 0).numpy()
outputs = self.vision_model.run(self.vision_outputs, {"pixel_values": pixel_values})
case str(): case str():
if self.mode == "vision": if self.mode == "vision":
raise TypeError("Cannot encode text as vision-only model") raise TypeError("Cannot encode text as vision-only model")
text_inputs: dict[str, torch.Tensor] = self.tokenizer(image_or_text)
inputs = { outputs = self.text_model.run(None, self.tokenize(image_or_text))
"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)
case _: case _:
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}") raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
return outputs[0][0].tolist() return outputs[0][0].tolist()
def _download_model(self, model_name: str, model_md5: str) -> bool: @abstractmethod
# downloading logic is adapted from clip-server's CLIPOnnxModel class def tokenize(self, text: str) -> dict[str, ndarray_i32]:
download_model( pass
url=_S3_BUCKET_V2 + model_name,
target_folder=self.cache_dir.as_posix(), @abstractmethod
md5sum=model_md5, def transform(self, image: Image.Image) -> dict[str, ndarray_f32]:
with_resume=True, pass
)
file = self.cache_dir / model_name.split("/")[1] @property
if file.suffix == ".zip": def textual_dir(self) -> Path:
with zipfile.ZipFile(file, "r") as zip_ref: return self.cache_dir / "textual"
zip_ref.extractall(self.cache_dir)
os.remove(file) @property
return True 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 @property
def cached(self) -> bool: 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` class OpenCLIPEncoder(BaseCLIPEncoder):
def _transform_pil_image(n_px: int) -> Compose: def __init__(
return Compose( self,
[ model_name: str,
Resize(n_px, interpolation=BICUBIC), cache_dir: str | None = None,
CenterCrop(n_px), mode: Literal["text", "vision"] | None = None,
_convert_image_to_rgb, **model_kwargs: Any,
ToTensor(), ) -> None:
Normalize( super().__init__(_clean_model_name(model_name), cache_dir, mode, **model_kwargs)
(0.48145466, 0.4578275, 0.40821073),
(0.26862954, 0.26130258, 0.27577711), 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.face_align import norm_crop
from insightface.utils.storage import BASE_REPO_URL, download_file 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 from .base import InferenceModel
@@ -68,7 +69,7 @@ class FaceRecognizer(InferenceModel):
) )
self.rec_model.prepare(ctx_id=0) 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): if isinstance(image, bytes):
image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR) image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR)
bboxes, kpss = self.det_model.detect(image) 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 enum import StrEnum
from typing import TypeAlias
import numpy as np
from pydantic import BaseModel from pydantic import BaseModel
@@ -31,3 +33,8 @@ class ModelType(StrEnum):
IMAGE_CLASSIFICATION = "image-classification" IMAGE_CLASSIFICATION = "image-classification"
CLIP = "clip" CLIP = "clip"
FACIAL_RECOGNITION = "facial-recognition" 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 json
import pickle import pickle
from io import BytesIO from io import BytesIO
from typing import Any, TypeAlias from pathlib import Path
from typing import Any, Callable
from unittest import mock from unittest import mock
import cv2 import cv2
@@ -14,13 +15,11 @@ from pytest_mock import MockerFixture
from .config import settings from .config import settings
from .models.base import PicklableSessionOptions from .models.base import PicklableSessionOptions
from .models.cache import ModelCache from .models.cache import ModelCache
from .models.clip import CLIPEncoder from .models.clip import OpenCLIPEncoder
from .models.facial_recognition import FaceRecognizer from .models.facial_recognition import FaceRecognizer
from .models.image_classification import ImageClassifier from .models.image_classification import ImageClassifier
from .schemas import ModelType from .schemas import ModelType
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
class TestImageClassifier: class TestImageClassifier:
classifier_preds = [ classifier_preds = [
@@ -56,30 +55,50 @@ class TestImageClassifier:
class TestCLIP: class TestCLIP:
embedding = np.random.rand(512).astype(np.float32) 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: def test_basic_image(
mocker.patch.object(CLIPEncoder, "download") 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 = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
mocked.return_value.run.return_value = [[self.embedding]] 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) embedding = clip_encoder.predict(pil_image)
assert clip_encoder.mode == "vision"
assert isinstance(embedding, list) 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]) assert all([isinstance(num, float) for num in embedding])
clip_encoder.vision_model.run.assert_called_once() clip_encoder.vision_model.run.assert_called_once()
def test_basic_text(self, mocker: MockerFixture) -> None: def test_basic_text(
mocker.patch.object(CLIPEncoder, "download") 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 = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
mocked.return_value.run.return_value = [[self.embedding]] 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") embedding = clip_encoder.predict("test search query")
assert clip_encoder.mode == "text"
assert isinstance(embedding, list) 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]) assert all([isinstance(num, float) for num in embedding])
clip_encoder.text_model.run.assert_called_once() 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 import json
from argparse import ArgumentParser
from io import BytesIO
from typing import Any from typing import Any
from locust import HttpUser, events, task from locust import HttpUser, events, task
from locust.env import Environment from locust.env import Environment
from PIL import Image from PIL import Image
from argparse import ArgumentParser
byte_image = BytesIO() 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("--tag-model", type=str, default="microsoft/resnet-50")
parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai") 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("--face-model", type=str, default="buffalo_l")
parser.add_argument("--tag-min-score", type=int, default=0.0, parser.add_argument(
help="Returns all tags at or above this score. The default returns all tags.") "--tag-min-score",
parser.add_argument("--face-min-score", type=int, default=0.034, type=int,
help=("Returns all faces at or above this score. The default returns 1 face per request; " default=0.0,
"setting this to 0 blows up the number of faces to the thousands.")) 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) parser.add_argument("--image-size", type=int, default=1000)
@@ -62,7 +73,7 @@ class CLIPTextFormDataLoadTest(InferenceLoadTest):
("modelName", self.environment.parsed_options.clip_model), ("modelName", self.environment.parsed_options.clip_model),
("modelType", "clip"), ("modelType", "clip"),
("options", json.dumps({"mode": "text"})), ("options", json.dumps({"mode": "text"})),
("text", "test search query") ("text", "test search query"),
] ]
self.client.post("/predict", data=data) self.client.post("/predict", data=data)
@@ -88,5 +99,5 @@ class RecognitionFormDataLoadTest(InferenceLoadTest):
("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})), ("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})),
] ]
files = {"image": self.data} files = {"image": self.data}
self.client.post("/predict", data=data, files=files) 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] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.83.0" version = "1.84.0"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
@@ -9,8 +9,8 @@ packages = [{include = "app"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
torch = [ torch = [
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.0.1", source = "pypi"}, {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.0.1", source = "pytorch-cpu"} {markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.1.0", source = "pytorch-cpu"}
] ]
transformers = "^4.29.2" transformers = "^4.29.2"
onnxruntime = "^1.15.0" onnxruntime = "^1.15.0"
@@ -22,14 +22,9 @@ uvicorn = {extras = ["standard"], version = "^0.22.0"}
pydantic = "^1.10.8" pydantic = "^1.10.8"
aiocache = "^0.12.1" aiocache = "^0.12.1"
optimum = "^1.9.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" rich = "^13.4.2"
ftfy = "^6.1.1" ftfy = "^6.1.1"
setuptools = "^68.0.0" setuptools = "^68.0.0"
open-clip-torch = "^2.20.0"
python-multipart = "^0.0.6" python-multipart = "^0.0.6"
orjson = "^3.9.5" orjson = "^3.9.5"
safetensors = "0.3.2" safetensors = "0.3.2"
@@ -63,6 +58,7 @@ warn_redundant_casts = true
disallow_any_generics = true disallow_any_generics = true
check_untyped_defs = true check_untyped_defs = true
disallow_untyped_defs = true disallow_untyped_defs = true
ignore_missing_imports = true
[tool.pydantic-mypy] [tool.pydantic-mypy]
init_forbid_extra = true init_forbid_extra = true
@@ -70,30 +66,6 @@ init_typed = true
warn_required_dynamic_aliases = true warn_required_dynamic_aliases = true
warn_untyped_fields = 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] [tool.ruff]
line-length = 120 line-length = 120
target-version = "py311" 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', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 107, "android.injected.version.code" => 108,
"android.injected.version.name" => "1.83.0", "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') 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 * 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

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.160108"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.176668"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484">
</testcase> </testcase>

View File

@@ -253,6 +253,8 @@
"profile_drawer_settings": "Settings", "profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out", "profile_drawer_sign_out": "Sign Out",
"profile_drawer_trash": "Trash", "profile_drawer_trash": "Trash",
"profile_drawer_documentation": "Documentation",
"profile_drawer_github": "GitHub",
"recently_added_page_title": "Recently Added", "recently_added_page_title": "Recently Added",
"search_bar_hint": "Search your photos", "search_bar_hint": "Search your photos",
"search_page_categories": "Categories", "search_page_categories": "Categories",
@@ -277,6 +279,7 @@
"select_user_for_sharing_page_share_suggestions": "Suggestions", "select_user_for_sharing_page_share_suggestions": "Suggestions",
"server_info_box_app_version": "App Version", "server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server 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_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_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", "setting_image_viewer_original_title": "Load original image",
@@ -311,6 +314,8 @@
"shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_change_expiry": "Change expiration time",
"shared_link_edit_description": "Description", "shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Enter the share 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_show_meta": "Show metadata",
"shared_link_edit_submit_button": "Update link", "shared_link_edit_submit_button": "Update link",
"shared_link_empty": "You don't have any shared links", "shared_link_empty": "You don't have any shared links",
@@ -364,5 +369,8 @@
"viewer_unstack": "Un-Stack", "viewer_unstack": "Un-Stack",
"cache_settings_tile_title": "Local Storage", "cache_settings_tile_title": "Local Storage",
"cache_settings_tile_subtitle": "Control the local storage behaviour", "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

@@ -28,6 +28,12 @@ PODS:
- Flutter - Flutter
- isar_flutter_libs (1.0.0): - isar_flutter_libs (1.0.0):
- Flutter - Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@@ -42,6 +48,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0):
- Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@@ -55,6 +63,8 @@ PODS:
- Flutter - Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
@@ -71,16 +81,21 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS: SPEC REPOS:
@@ -115,6 +130,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
isar_flutter_libs: isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios" :path: ".symlinks/plugins/isar_flutter_libs/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@@ -125,6 +146,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager: photo_manager:
:path: ".symlinks/plugins/photo_manager/ios" :path: ".symlinks/plugins/photo_manager/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@@ -135,6 +158,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios" :path: ".symlinks/plugins/video_player_avfoundation/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus: wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
@@ -152,6 +177,9 @@ SPEC CHECKSUMS:
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670 integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
@@ -159,14 +187,16 @@ SPEC CHECKSUMS:
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.12.1 COCOAPODS: 1.11.3

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 118; CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 118; CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 118; CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -59,11 +59,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.78.1</string> <string>1.84.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>118</string> <string>124</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true /> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.83.0" version_number: "1.84.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000256"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="7.645306"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.669798"> <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.218788"> <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="97.596654"> <testcase classname="fastlane.lanes" name="4: build_app" time="145.399278">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="89.490906"> <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235">
</testcase> </testcase>

View File

@@ -35,6 +35,7 @@ import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/migration.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
void main() async { void main() async {
@@ -49,6 +50,7 @@ void main() async {
Future<void> initApp() async { Future<void> initApp() async {
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
MediaKit.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) { if (kReleaseMode && Platform.isAndroid) {
try { try {

View File

@@ -265,6 +265,7 @@ class AlbumViewerPage extends HookConsumerWidget {
if (data.isRemote) buildControlButton(data), 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/shared/models/album.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/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 { class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key); const LibraryPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final albums = ref.watch(albumProvider); final albums = ref.watch(albumProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var settings = ref.watch(appSettingsServiceProvider); 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 = final selectedAlbumSortOrder =
useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder)); useState(settings.getSetting(AppSettingsEnum.selectedAlbumSortOrder));
@@ -236,8 +225,23 @@ class LibraryPage extends HookConsumerWidget {
final local = albums.where((a) => a.isLocal).toList(); 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( return Scaffold(
appBar: buildAppBar(), appBar: ImmichAppBar(
action: shareTrashButton(),
),
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( 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/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/providers/user.provider.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'; import 'package:immich_mobile/shared/ui/immich_image.dart';
class SharingPage extends HookConsumerWidget { 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() { buildEmptyListIndication() {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( 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( return Scaffold(
appBar: buildAppBar(), appBar: ImmichAppBar(
action: sharePartnerButton(),
),
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter(child: buildTopBottons()), SliverToBoxAdapter(child: buildTopBottons()),

View File

@@ -31,7 +31,14 @@ class DescriptionInput extends HookConsumerWidget {
final owner = ref.watch(currentUserProvider); final owner = ref.watch(currentUserProvider);
final hasError = useState(false); final hasError = useState(false);
controller.text = description; useEffect(
() {
controller.text = description;
isTextEmpty.value = description.isEmpty;
return null;
},
[description],
);
submitDescription(String description) async { submitDescription(String description) async {
hasError.value = false; hasError.value = false;

View File

@@ -297,9 +297,9 @@ class ExifBottomSheet extends HookConsumerWidget {
fontWeight: FontWeight.bold, 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 ?? ''} ", "ƒ/${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.isPlayingMotionVideo,
required this.onFavorite, required this.onFavorite,
required this.onUploadPressed, required this.onUploadPressed,
required this.isOwner,
}) : super(key: key); }) : super(key: key);
final Asset asset; final Asset asset;
@@ -25,6 +26,7 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback onAddToAlbumPressed; final VoidCallback onAddToAlbumPressed;
final Function(Asset) onFavorite; final Function(Asset) onFavorite;
final bool isPlayingMotionVideo; final bool isPlayingMotionVideo;
final bool isOwner;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -123,11 +125,11 @@ class TopControlAppBar extends HookConsumerWidget {
size: iconSize, size: iconSize,
), ),
actions: [ actions: [
if (asset.isRemote) buildFavoriteButton(a), if (asset.isRemote && isOwner) buildFavoriteButton(a),
if (asset.livePhotoVideoId != null) buildLivePhotoButton(), if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal) buildDownloadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
if (asset.isRemote) buildAddToAlbumButtom(), if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
buildMoreInfoButton(), buildMoreInfoButton(),
], ],
); );

View File

@@ -48,6 +48,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final int initialIndex; final int initialIndex;
final int heroOffset; final int heroOffset;
final bool showStack; final bool showStack;
final bool isOwner;
GalleryViewerPage({ GalleryViewerPage({
super.key, super.key,
@@ -56,6 +57,7 @@ class GalleryViewerPage extends HookConsumerWidget {
required this.totalAssets, required this.totalAssets,
this.heroOffset = 0, this.heroOffset = 0,
this.showStack = false, this.showStack = false,
this.isOwner = true,
}) : controller = PageController(initialPage: initialIndex); }) : controller = PageController(initialPage: initialIndex);
final PageController controller; final PageController controller;
@@ -88,7 +90,7 @@ class GalleryViewerPage extends HookConsumerWidget {
: <Asset>[]; : <Asset>[];
final stackElements = showStack ? [currentAsset, ...stack] : <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 // 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 Asset asset() => stackIndex.value == -1
? currentAsset ? currentAsset
@@ -334,6 +336,7 @@ class GalleryViewerPage extends HookConsumerWidget {
child: Container( child: Container(
color: Colors.black.withOpacity(0.4), color: Colors.black.withOpacity(0.4),
child: TopControlAppBar( child: TopControlAppBar(
isOwner: isOwner,
isPlayingMotionVideo: isPlayingMotionVideo.value, isPlayingMotionVideo: isPlayingMotionVideo.value,
asset: asset(), asset: asset(),
onMoreInfoPressed: showInfo, onMoreInfoPressed: showInfo,
@@ -573,35 +576,50 @@ class GalleryViewerPage extends HookConsumerWidget {
label: 'control_bottom_app_bar_share'.tr(), label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(), tooltip: 'control_bottom_app_bar_share'.tr(),
), ),
asset().isArchived if (isOwner)
? BottomNavigationBarItem( asset().isArchived
icon: const Icon(Icons.unarchive_rounded), ? BottomNavigationBarItem(
label: 'control_bottom_app_bar_unarchive'.tr(), icon: const Icon(Icons.unarchive_rounded),
tooltip: 'control_bottom_app_bar_unarchive'.tr(), label: 'control_bottom_app_bar_unarchive'.tr(),
) tooltip: 'control_bottom_app_bar_unarchive'.tr(),
: BottomNavigationBarItem( )
icon: const Icon(Icons.archive_outlined), : BottomNavigationBarItem(
label: 'control_bottom_app_bar_archive'.tr(), icon: const Icon(Icons.archive_outlined),
tooltip: 'control_bottom_app_bar_archive'.tr(), label: 'control_bottom_app_bar_archive'.tr(),
), tooltip: 'control_bottom_app_bar_archive'.tr(),
if (stack.isNotEmpty) ),
if (isOwner && stack.isNotEmpty)
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined), icon: const Icon(Icons.burst_mode_outlined),
label: 'control_bottom_app_bar_stack'.tr(), label: 'control_bottom_app_bar_stack'.tr(),
tooltip: 'control_bottom_app_bar_stack'.tr(), tooltip: 'control_bottom_app_bar_stack'.tr(),
), ),
BottomNavigationBarItem( if (isOwner)
icon: const Icon(Icons.delete_outline), BottomNavigationBarItem(
label: 'control_bottom_app_bar_delete'.tr(), icon: const Icon(Icons.delete_outline),
tooltip: 'control_bottom_app_bar_delete'.tr(), 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 = [ List<Function(int)> actionslist = [
(_) => shareAsset(), (_) => shareAsset(),
(_) => handleArchive(asset()), if (isOwner) (_) => handleArchive(asset()),
if (stack.isNotEmpty) (_) => showStackActionItems(), if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
(_) => handleDelete(asset()), if (isOwner) (_) => handleDelete(asset()),
if (!isOwner)
(_) => asset().isLocal
? null
: ref.watch(imageViewerStateProvider.notifier).downloadAsset(
asset(),
context,
),
]; ];
return IgnorePointer( return IgnorePointer(
@@ -755,7 +773,7 @@ class GalleryViewerPage extends HookConsumerWidget {
}, },
imageProvider: provider, imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: isFromResponse tag: isFromDto
? '${a.remoteId}-$heroOffset' ? '${a.remoteId}-$heroOffset'
: a.id + heroOffset, : a.id + heroOffset,
), ),
@@ -773,11 +791,6 @@ class GalleryViewerPage extends HookConsumerWidget {
localPosition = details.localPosition, localPosition = details.localPosition,
onDragUpdate: (_, details, __) => onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details), handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(
tag: isFromResponse
? '${a.remoteId}-$heroOffset'
: a.id + heroOffset,
),
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
maxScale: 1.0, maxScale: 1.0,
minScale: 1.0, minScale: 1.0,

View File

@@ -9,6 +9,8 @@ import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
@@ -125,99 +127,128 @@ class VideoPlayer extends StatefulWidget {
} }
class _VideoPlayerState extends State<VideoPlayer> { class _VideoPlayerState extends State<VideoPlayer> {
late VideoPlayerController videoPlayerController; // late VideoPlayerController videoPlayerController;
ChewieController? chewieController; // ChewieController? chewieController;
// Create a [Player] to control playback.
late final player = Player();
// Create a [VideoController] to handle video output from [Player].
late final controller = VideoController(player);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initializePlayer(); // initializePlayer();
videoPlayerController.addListener(() { // videoPlayerController.addListener(() {
if (videoPlayerController.value.isInitialized) { // if (videoPlayerController.value.isInitialized) {
if (videoPlayerController.value.isPlaying) { // if (videoPlayerController.value.isPlaying) {
WakelockPlus.enable(); // WakelockPlus.enable();
widget.onPlaying?.call(); // widget.onPlaying?.call();
} else if (!videoPlayerController.value.isPlaying) { // } else if (!videoPlayerController.value.isPlaying) {
WakelockPlus.disable(); // WakelockPlus.disable();
widget.onPaused?.call(); // widget.onPaused?.call();
} // }
if (videoPlayerController.value.position == // if (videoPlayerController.value.position ==
videoPlayerController.value.duration) { // videoPlayerController.value.duration) {
WakelockPlus.disable(); // WakelockPlus.disable();
widget.onVideoEnded(); // widget.onVideoEnded();
} // }
} // }
}); // });
}
Future<void> initializePlayer() async { if (widget.file == null) {
try { player.open(
videoPlayerController = widget.file == null Media(
? VideoPlayerController.networkUrl( Uri.parse(widget.url!).toString(),
Uri.parse(widget.url!), httpHeaders: {
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}, "Authorization": "Bearer ${widget.jwtToken}",
) },
: VideoPlayerController.file(widget.file!); ),
);
await videoPlayerController.initialize(); } else {
_createChewieController(); player.open(
setState(() {}); Media(
} catch (e) { widget.file!.path,
debugPrint("ERROR initialize video player $e"); ),
);
} }
} }
_createChewieController() { // Future<void> initializePlayer() async {
chewieController = ChewieController( // try {
controlsSafeAreaMinimum: const EdgeInsets.only( // videoPlayerController = widget.file == null
bottom: 100, // ? VideoPlayerController.networkUrl(
), // Uri.parse(widget.url!),
showOptions: true, // httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
showControlsOnInitialize: false, // )
videoPlayerController: videoPlayerController, // : VideoPlayerController.file(widget.file!);
autoPlay: true,
autoInitialize: true, // await videoPlayerController.initialize();
allowFullScreen: false, // _createChewieController();
allowedScreenSleep: false, // setState(() {});
showControls: !widget.isMotionVideo, // } catch (e) {
customControls: const VideoPlayerControls(), // debugPrint("ERROR initialize video player $e");
hideControlsTimer: const Duration(seconds: 5), // }
); // }
}
// _createChewieController() {
// chewieController = ChewieController(
// controlsSafeAreaMinimum: const EdgeInsets.only(
// bottom: 100,
// ),
// showOptions: true,
// showControlsOnInitialize: false,
// videoPlayerController: videoPlayerController,
// autoPlay: true,
// autoInitialize: true,
// allowFullScreen: false,
// allowedScreenSleep: false,
// showControls: !widget.isMotionVideo,
// customControls: const VideoPlayerControls(),
// hideControlsTimer: const Duration(seconds: 5),
// );
// }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
videoPlayerController.pause(); player.dispose();
videoPlayerController.dispose(); // videoPlayerController.pause();
chewieController?.dispose(); // videoPlayerController.dispose();
// chewieController?.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (chewieController?.videoPlayerController.value.isInitialized == true) { return SizedBox(
return SizedBox( width: MediaQuery.of(context).size.width,
child: Chewie( height: MediaQuery.of(context).size.height,
controller: chewieController!, child: Video(controller: controller),
), );
);
} else { // if (chewieController?.videoPlayerController.value.isInitialized == true) {
return SizedBox( // return SizedBox(
height: MediaQuery.of(context).size.height, // child: Chewie(
width: MediaQuery.of(context).size.width, // controller: chewieController!,
child: Center( // ),
child: Stack( // );
children: [ // } else {
if (widget.placeholder != null) widget.placeholder!, // return SizedBox(
const Center( // height: MediaQuery.of(context).size.height,
child: ImmichLoadingIndicator(), // width: MediaQuery.of(context).size.width,
), // child: Center(
], // child: Stack(
), // children: [
), // if (widget.placeholder != null) widget.placeholder!,
); // const Center(
} // child: ImmichLoadingIndicator(),
// ),
// ],
// ),
// ),
// );
// }
} }
} }

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() { ListTile buildAutoBackupController() {
final isAutoBackup = backupState.autoBackup; final isAutoBackup = backupState.autoBackup;
final backUpOption = isAutoBackup final backUpOption = isAutoBackup
@@ -774,7 +734,6 @@ class BackupControllerPage extends HookConsumerWidget {
if (showBackupFix) const Divider(), if (showBackupFix) const Divider(),
if (showBackupFix) buildCheckCorruptBackups(), if (showBackupFix) buildCheckCorruptBackups(),
const Divider(), const Divider(),
buildStorageInformation(),
const Divider(), const Divider(),
const CurrentUploadingAssetInfoBox(), const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(), if (!hasExclusiveAccess) buildBackgroundBackupInfo(),

View File

@@ -33,6 +33,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool shrinkWrap; final bool shrinkWrap;
final bool showDragScroll; final bool showDragScroll;
final bool showStack; final bool showStack;
final bool isOwner;
const ImmichAssetGrid({ const ImmichAssetGrid({
super.key, super.key,
@@ -53,6 +54,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.shrinkWrap = false, this.shrinkWrap = false,
this.showDragScroll = true, this.showDragScroll = true,
this.showStack = false, this.showStack = false,
this.isOwner = true,
}); });
@override @override
@@ -117,6 +119,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
showDragScroll: showDragScroll, showDragScroll: showDragScroll,
showStack: showStack, showStack: showStack,
isOwner: isOwner,
), ),
); );
} }

View File

@@ -38,6 +38,7 @@ class ImmichAssetGridView extends StatefulWidget {
final bool shrinkWrap; final bool shrinkWrap;
final bool showDragScroll; final bool showDragScroll;
final bool showStack; final bool showStack;
final bool isOwner;
const ImmichAssetGridView({ const ImmichAssetGridView({
super.key, super.key,
@@ -58,6 +59,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.shrinkWrap = false, this.shrinkWrap = false,
this.showDragScroll = true, this.showDragScroll = true,
this.showStack = false, this.showStack = false,
this.isOwner = true,
}); });
@override @override
@@ -138,6 +140,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
showStorageIndicator: widget.showStorageIndicator, showStorageIndicator: widget.showStorageIndicator,
heroOffset: widget.heroOffset, heroOffset: widget.heroOffset,
showStack: widget.showStack, showStack: widget.showStack,
isOwner: widget.isOwner,
); );
} }

View File

@@ -14,6 +14,7 @@ class ThumbnailImage extends StatelessWidget {
final int totalAssets; final int totalAssets;
final bool showStorageIndicator; final bool showStorageIndicator;
final bool showStack; final bool showStack;
final bool isOwner;
final bool useGrayBoxPlaceholder; final bool useGrayBoxPlaceholder;
final bool isSelected; final bool isSelected;
final bool multiselectEnabled; final bool multiselectEnabled;
@@ -29,6 +30,7 @@ class ThumbnailImage extends StatelessWidget {
required this.totalAssets, required this.totalAssets,
this.showStorageIndicator = true, this.showStorageIndicator = true,
this.showStack = false, this.showStack = false,
this.isOwner = true,
this.useGrayBoxPlaceholder = false, this.useGrayBoxPlaceholder = false,
this.isSelected = false, this.isSelected = false,
this.multiselectEnabled = false, this.multiselectEnabled = false,
@@ -43,7 +45,7 @@ class ThumbnailImage extends StatelessWidget {
final assetContainerColor = final assetContainerColor =
isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight; 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 // 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) { Widget buildSelectionIcon(Asset asset) {
if (isSelected) { if (isSelected) {
@@ -132,7 +134,7 @@ class ThumbnailImage extends StatelessWidget {
width: 300, width: 300,
height: 300, height: 300,
child: Hero( child: Hero(
tag: isFromResponse tag: isFromDto
? '${asset.remoteId}-$heroOffset' ? '${asset.remoteId}-$heroOffset'
: asset.id + heroOffset, : asset.id + heroOffset,
child: ImmichImage( child: ImmichImage(
@@ -181,6 +183,7 @@ class ThumbnailImage extends StatelessWidget {
totalAssets: totalAssets, totalAssets: totalAssets,
heroOffset: heroOffset, heroOffset: heroOffset,
showStack: showStack, 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/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.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/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/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/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.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/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/user.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/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_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/selection_handlers.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() { Widget buildBody() {
void selectionListener( void selectionListener(
bool multiselect, bool multiselect,
@@ -375,10 +370,7 @@ class HomePage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
appBar: !selectionEnabledHook.value appBar: !selectionEnabledHook.value ? const ImmichAppBar() : null,
? HomePageAppBar(onPopBack: reloadAllAsset)
: null,
drawer: const ProfileDrawer(),
body: buildBody(), body: buildBody(),
); );
} }

View File

@@ -16,7 +16,8 @@ class MemoryLane extends HookConsumerWidget {
final memoryLane = memoryLaneFutureProvider final memoryLane = memoryLaneFutureProvider
.whenData( .whenData(
(memories) => memories != null (memories) => memories != null
? SizedBox( ? Container(
margin: const EdgeInsets.only(top: 10),
height: 200, height: 200,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,

View File

@@ -9,6 +9,7 @@ class SharedLink {
final bool allowUpload; final bool allowUpload;
final String? thumbAssetId; final String? thumbAssetId;
final String? description; final String? description;
final String? password;
final DateTime? expiresAt; final DateTime? expiresAt;
final String key; final String key;
final bool showMetadata; final bool showMetadata;
@@ -21,6 +22,7 @@ class SharedLink {
required this.allowUpload, required this.allowUpload,
required this.thumbAssetId, required this.thumbAssetId,
required this.description, required this.description,
required this.password,
required this.expiresAt, required this.expiresAt,
required this.key, required this.key,
required this.showMetadata, required this.showMetadata,
@@ -34,6 +36,7 @@ class SharedLink {
bool? allowDownload, bool? allowDownload,
bool? allowUpload, bool? allowUpload,
String? description, String? description,
String? password,
DateTime? expiresAt, DateTime? expiresAt,
String? key, String? key,
bool? showMetadata, bool? showMetadata,
@@ -46,6 +49,7 @@ class SharedLink {
allowDownload: allowDownload ?? this.allowDownload, allowDownload: allowDownload ?? this.allowDownload,
allowUpload: allowUpload ?? this.allowUpload, allowUpload: allowUpload ?? this.allowUpload,
description: description ?? this.description, description: description ?? this.description,
password: password ?? this.password,
expiresAt: expiresAt ?? this.expiresAt, expiresAt: expiresAt ?? this.expiresAt,
key: key ?? this.key, key: key ?? this.key,
showMetadata: showMetadata ?? this.showMetadata, showMetadata: showMetadata ?? this.showMetadata,
@@ -58,6 +62,7 @@ class SharedLink {
allowDownload = dto.allowDownload, allowDownload = dto.allowDownload,
allowUpload = dto.allowUpload, allowUpload = dto.allowUpload,
description = dto.description, description = dto.description,
password = dto.password,
expiresAt = dto.expiresAt, expiresAt = dto.expiresAt,
key = dto.key, key = dto.key,
showMetadata = dto.showMetadata, showMetadata = dto.showMetadata,
@@ -75,7 +80,7 @@ class SharedLink {
@override @override
String toString() => 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 @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -87,6 +92,7 @@ class SharedLink {
other.allowDownload == allowDownload && other.allowDownload == allowDownload &&
other.allowUpload == allowUpload && other.allowUpload == allowUpload &&
other.description == description && other.description == description &&
other.password == password &&
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.key == key && other.key == key &&
other.showMetadata == showMetadata && other.showMetadata == showMetadata &&
@@ -100,6 +106,7 @@ class SharedLink {
allowDownload.hashCode ^ allowDownload.hashCode ^
allowUpload.hashCode ^ allowUpload.hashCode ^
description.hashCode ^ description.hashCode ^
password.hashCode ^
expiresAt.hashCode ^ expiresAt.hashCode ^
key.hashCode ^ key.hashCode ^
showMetadata.hashCode ^ showMetadata.hashCode ^

View File

@@ -40,6 +40,7 @@ class SharedLinkService {
required bool allowDownload, required bool allowDownload,
required bool allowUpload, required bool allowUpload,
String? description, String? description,
String? password,
String? albumId, String? albumId,
List<String>? assetIds, List<String>? assetIds,
DateTime? expiresAt, DateTime? expiresAt,
@@ -57,6 +58,7 @@ class SharedLinkService {
allowUpload: allowUpload, allowUpload: allowUpload,
expiresAt: expiresAt, expiresAt: expiresAt,
description: description, description: description,
password: password,
); );
} else if (assetIds != null) { } else if (assetIds != null) {
dto = SharedLinkCreateDto( dto = SharedLinkCreateDto(
@@ -66,6 +68,7 @@ class SharedLinkService {
allowUpload: allowUpload, allowUpload: allowUpload,
expiresAt: expiresAt, expiresAt: expiresAt,
description: description, description: description,
password: password,
assetIds: assetIds, assetIds: assetIds,
); );
} }
@@ -90,6 +93,7 @@ class SharedLinkService {
required bool? allowUpload, required bool? allowUpload,
bool? changeExpiry = false, bool? changeExpiry = false,
String? description, String? description,
String? password,
DateTime? expiresAt, DateTime? expiresAt,
}) async { }) async {
try { try {
@@ -101,6 +105,7 @@ class SharedLinkService {
allowUpload: allowUpload, allowUpload: allowUpload,
expiresAt: expiresAt, expiresAt: expiresAt,
description: description, description: description,
password: password,
changeExpiryTime: changeExpiry, changeExpiryTime: changeExpiry,
), ),
); );

View File

@@ -30,6 +30,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
final descriptionController = final descriptionController =
useTextEditingController(text: existingLink?.description ?? ""); useTextEditingController(text: existingLink?.description ?? "");
final descriptionFocusNode = useFocusNode(); final descriptionFocusNode = useFocusNode();
final passwordController =
useTextEditingController(text: existingLink?.password ?? "");
final showMetadata = useState(existingLink?.showMetadata ?? true); final showMetadata = useState(existingLink?.showMetadata ?? true);
final allowDownload = useState(existingLink?.allowDownload ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true);
final allowUpload = useState(existingLink?.allowUpload ?? false); 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() { Widget buildShowMetaButton() {
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
value: showMetadata.value, value: showMetadata.value,
@@ -229,7 +256,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
void copyLinkToClipboard() { void copyLinkToClipboard() {
Clipboard.setData( Clipboard.setData(
ClipboardData( ClipboardData(
text: newShareLink.value, text: passwordController.text.isEmpty
? newShareLink.value
: "Link: ${newShareLink.value}\nPassword: ${passwordController.text}",
), ),
).then((_) { ).then((_) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -302,6 +331,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
description: descriptionController.text.isEmpty description: descriptionController.text.isEmpty
? null ? null
: descriptionController.text, : descriptionController.text,
password: passwordController.text.isEmpty
? null
: passwordController.text,
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
); );
ref.invalidate(sharedLinksStateProvider); ref.invalidate(sharedLinksStateProvider);
@@ -324,6 +356,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
bool? upload; bool? upload;
bool? meta; bool? meta;
String? desc; String? desc;
String? password;
DateTime? expiry; DateTime? expiry;
bool? changeExpiry; bool? changeExpiry;
@@ -343,6 +376,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
desc = descriptionController.text; desc = descriptionController.text;
} }
if (passwordController.text != existingLink!.password) {
password = passwordController.text;
}
if (editExpiry.value) { if (editExpiry.value) {
expiry = expiryAfter.value == 0 ? null : calculateExpiry(); expiry = expiryAfter.value == 0 ? null : calculateExpiry();
changeExpiry = true; changeExpiry = true;
@@ -354,6 +391,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
allowDownload: download, allowDownload: download,
allowUpload: upload, allowUpload: upload,
description: desc, description: desc,
password: password,
expiresAt: expiry, expiresAt: expiry,
changeExpiry: changeExpiry, changeExpiry: changeExpiry,
); );
@@ -385,6 +423,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
padding: const EdgeInsets.all(padding), padding: const EdgeInsets.all(padding),
child: buildDescriptionField(), child: buildDescriptionField(),
), ),
Padding(
padding: const EdgeInsets.all(padding),
child: buildPasswordField(),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: padding, left: padding,

View File

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

View File

@@ -72,6 +72,7 @@ class _$AppRouter extends RootStackRouter {
totalAssets: args.totalAssets, totalAssets: args.totalAssets,
heroOffset: args.heroOffset, heroOffset: args.heroOffset,
showStack: args.showStack, showStack: args.showStack,
isOwner: args.isOwner,
), ),
); );
}, },
@@ -230,12 +231,9 @@ class _$AppRouter extends RootStackRouter {
); );
}, },
AppLogRoute.name: (routeData) { AppLogRoute.name: (routeData) {
return CustomPage<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
child: const AppLogPage(), child: const AppLogPage(),
transitionsBuilder: TransitionsBuilders.slideBottom,
opaque: true,
barrierDismissible: false,
); );
}, },
AppLogDetailRoute.name: (routeData) { AppLogDetailRoute.name: (routeData) {
@@ -582,6 +580,7 @@ class _$AppRouter extends RootStackRouter {
RouteConfig( RouteConfig(
AppLogRoute.name, AppLogRoute.name,
path: '/app-log-page', path: '/app-log-page',
guards: [duplicateGuard],
), ),
RouteConfig( RouteConfig(
AppLogDetailRoute.name, AppLogDetailRoute.name,
@@ -749,6 +748,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
required int totalAssets, required int totalAssets,
int heroOffset = 0, int heroOffset = 0,
bool showStack = false, bool showStack = false,
bool isOwner = true,
}) : super( }) : super(
GalleryViewerRoute.name, GalleryViewerRoute.name,
path: '/gallery-viewer-page', path: '/gallery-viewer-page',
@@ -759,6 +759,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
totalAssets: totalAssets, totalAssets: totalAssets,
heroOffset: heroOffset, heroOffset: heroOffset,
showStack: showStack, showStack: showStack,
isOwner: isOwner,
), ),
); );
@@ -773,6 +774,7 @@ class GalleryViewerRouteArgs {
required this.totalAssets, required this.totalAssets,
this.heroOffset = 0, this.heroOffset = 0,
this.showStack = false, this.showStack = false,
this.isOwner = true,
}); });
final Key? key; final Key? key;
@@ -787,9 +789,11 @@ class GalleryViewerRouteArgs {
final bool showStack; final bool showStack;
final bool isOwner;
@override @override
String toString() { 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:isar/isar.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart';
part 'exif_info.g.dart'; part 'exif_info.g.dart';
@@ -165,7 +164,11 @@ double? _exposureTimeToSeconds(String? s) {
} }
final parts = s.split("/"); final parts = s.split("/");
if (parts.length == 2) { 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; 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.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/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class ProfileDrawerHeader extends HookConsumerWidget { class AppBarProfileInfoBox extends HookConsumerWidget {
const ProfileDrawerHeader({ const AppBarProfileInfoBox({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -23,30 +22,24 @@ class ProfileDrawerHeader extends HookConsumerWidget {
final user = Store.tryGet(StoreKey.currentUser); final user = Store.tryGet(StoreKey.currentUser);
buildUserProfileImage() { buildUserProfileImage() {
const immichImage = CircleAvatar(
radius: 20,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
if (authState.profileImagePath.isEmpty || user == null) { if (authState.profileImagePath.isEmpty || user == null) {
return const CircleAvatar( return immichImage;
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
} }
var userImage = UserCircleAvatar( final userImage = UserCircleAvatar(
radius: 35, radius: 20,
size: 66, size: 40,
user: user, user: user,
); );
if (uploadProfileImageStatus == UploadProfileStatus.idle) { if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (authState.profileImagePath.isNotEmpty) { return authState.profileImagePath.isNotEmpty ? userImage : immichImage;
return userImage;
} else {
return const CircleAvatar(
radius: 33,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
} }
if (uploadProfileImageStatus == UploadProfileStatus.success) { if (uploadProfileImageStatus == UploadProfileStatus.success) {
@@ -54,18 +47,18 @@ class ProfileDrawerHeader extends HookConsumerWidget {
} }
if (uploadProfileImageStatus == UploadProfileStatus.failure) { if (uploadProfileImageStatus == UploadProfileStatus.failure) {
return const CircleAvatar( return immichImage;
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
} }
if (uploadProfileImageStatus == UploadProfileStatus.loading) { 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 { pickUserProfileImage() async {
@@ -80,54 +73,45 @@ class ProfileDrawerHeader extends HookConsumerWidget {
await ref.watch(uploadProfileImageProvider.notifier).upload(image); await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) { if (success) {
final profileImagePath =
ref.read(uploadProfileImageProvider).profileImagePath;
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath( ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
ref.read(uploadProfileImageProvider).profileImagePath, profileImagePath,
); );
if (user != null) {
user.profileImagePath = profileImagePath;
Store.put(StoreKey.currentUser, user);
}
} }
} }
} }
useEffect( return Padding(
() { padding: const EdgeInsets.symmetric(horizontal: 10.0),
// buildUserProfileImage(); child: Container(
return null; width: double.infinity,
}, decoration: BoxDecoration(
[], color: Theme.of(context).brightness == Brightness.dark
); ? Theme.of(context).scaffoldBackgroundColor
: const Color.fromARGB(255, 225, 229, 240),
return DrawerHeader( borderRadius: const BorderRadius.only(
decoration: BoxDecoration( topLeft: Radius.circular(10),
gradient: LinearGradient( topRight: Radius.circular(10),
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,
), ),
), child: ListTile(
child: Column( minLeadingWidth: 50,
mainAxisAlignment: MainAxisAlignment.start, leading: GestureDetector(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: pickUserProfileImage, onTap: pickUserProfileImage,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
buildUserProfileImage(), buildUserProfileImage(),
Positioned( Positioned(
bottom: 0, bottom: -5,
right: -5, right: -8,
child: Material( child: Material(
color: isDarkMode ? Colors.grey[700] : Colors.grey[100], color: isDarkMode ? Colors.blueGrey[800] : Colors.white,
elevation: 3, elevation: 3,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0), borderRadius: BorderRadius.circular(50.0),
@@ -135,7 +119,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: Icon( child: Icon(
Icons.edit, Icons.camera_alt_outlined,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
size: 14, size: 14,
), ),
@@ -145,19 +129,21 @@ class ProfileDrawerHeader extends HookConsumerWidget {
], ],
), ),
), ),
Text( title: Text(
"${authState.firstName} ${authState.lastName}", "${authState.firstName} ${authState.lastName}",
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 24, fontSize: 16,
), ),
), ),
Text( subtitle: Text(
authState.userEmail, 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'; import 'package:flutter/material.dart';
class ImmichLoadingIndicator extends StatelessWidget { class ImmichLoadingIndicator extends StatelessWidget {
final double? borderRadius;
const ImmichLoadingIndicator({ const ImmichLoadingIndicator({
Key? key, Key? key,
this.borderRadius,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -12,7 +15,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
width: 60, width: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200), color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(borderRadius ?? 10),
), ),
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
child: const CircularProgressIndicator( child: const CircularProgressIndicator(

View File

@@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
@@ -46,7 +47,7 @@ class UserCircleAvatar extends ConsumerWidget {
radius: radius, radius: radius,
child: user.profileImagePath == "" child: user.profileImagePath == ""
? Text( ? Text(
user.firstName[0], user.firstName[0].toUpperCase(),
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black, color: Colors.black,
@@ -54,19 +55,18 @@ class UserCircleAvatar extends ConsumerWidget {
) )
: ClipRRect( : ClipRRect(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
child: FadeInImage( child: CachedNetworkImage(
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage), cacheKey: user.profileImagePath,
width: size, width: size,
height: size, height: size,
image: NetworkImage( placeholder: (_, __) => Image.memory(kTransparentImage),
profileImageUrl, imageUrl: profileImageUrl,
headers: { httpHeaders: {
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}", "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
}, },
), fadeInDuration: const Duration(milliseconds: 300),
fadeInDuration: const Duration(milliseconds: 200), errorWidget: (context, error, stackTrace) =>
imageErrorBuilder: (context, error, stackTrace) =>
Image.memory(kTransparentImage), Image.memory(kTransparentImage),
), ),
), ),

View File

@@ -1,6 +1,7 @@
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
String getThumbnailUrl( String getThumbnailUrl(
@@ -35,8 +36,10 @@ String getAlbumThumbnailUrl(
if (album.thumbnail.value?.remoteId == null) { if (album.thumbnail.value?.remoteId == null) {
return ''; return '';
} }
return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, return getThumbnailUrlForRemoteId(
type: type,); album.thumbnail.value!.remoteId!,
type: type,
);
} }
String getAlbumThumbNailCacheKey( String getAlbumThumbNailCacheKey(
@@ -57,7 +60,9 @@ String getImageUrl(final Asset asset) {
} }
String getImageCacheKey(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( String getThumbnailUrlForRemoteId(

View File

@@ -8,6 +8,10 @@ doc/APIKeyCreateDto.md
doc/APIKeyCreateResponseDto.md doc/APIKeyCreateResponseDto.md
doc/APIKeyResponseDto.md doc/APIKeyResponseDto.md
doc/APIKeyUpdateDto.md doc/APIKeyUpdateDto.md
doc/ActivityApi.md
doc/ActivityCreateDto.md
doc/ActivityResponseDto.md
doc/ActivityStatisticsResponseDto.md
doc/AddUsersDto.md doc/AddUsersDto.md
doc/AdminSignupResponseDto.md doc/AdminSignupResponseDto.md
doc/AlbumApi.md doc/AlbumApi.md
@@ -97,6 +101,7 @@ doc/PersonResponseDto.md
doc/PersonStatisticsResponseDto.md doc/PersonStatisticsResponseDto.md
doc/PersonUpdateDto.md doc/PersonUpdateDto.md
doc/QueueStatusDto.md doc/QueueStatusDto.md
doc/ReactionType.md
doc/RecognitionConfig.md doc/RecognitionConfig.md
doc/ScanLibraryDto.md doc/ScanLibraryDto.md
doc/SearchAlbumResponseDto.md doc/SearchAlbumResponseDto.md
@@ -128,6 +133,8 @@ doc/SystemConfigApi.md
doc/SystemConfigDto.md doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md doc/SystemConfigFFmpegDto.md
doc/SystemConfigJobDto.md doc/SystemConfigJobDto.md
doc/SystemConfigLibraryDto.md
doc/SystemConfigLibraryScanDto.md
doc/SystemConfigMachineLearningDto.md doc/SystemConfigMachineLearningDto.md
doc/SystemConfigMapDto.md doc/SystemConfigMapDto.md
doc/SystemConfigNewVersionCheckDto.md doc/SystemConfigNewVersionCheckDto.md
@@ -156,12 +163,13 @@ doc/UpdateTagDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
doc/UsageByUserDto.md doc/UsageByUserDto.md
doc/UserApi.md doc/UserApi.md
doc/UserCountResponseDto.md doc/UserDto.md
doc/UserResponseDto.md doc/UserResponseDto.md
doc/ValidateAccessTokenResponseDto.md doc/ValidateAccessTokenResponseDto.md
doc/VideoCodec.md doc/VideoCodec.md
git_push.sh git_push.sh
lib/api.dart lib/api.dart
lib/api/activity_api.dart
lib/api/album_api.dart lib/api/album_api.dart
lib/api/api_key_api.dart lib/api/api_key_api.dart
lib/api/asset_api.dart lib/api/asset_api.dart
@@ -186,6 +194,9 @@ lib/auth/authentication.dart
lib/auth/http_basic_auth.dart lib/auth/http_basic_auth.dart
lib/auth/http_bearer_auth.dart lib/auth/http_bearer_auth.dart
lib/auth/oauth.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/add_users_dto.dart
lib/model/admin_signup_response_dto.dart lib/model/admin_signup_response_dto.dart
lib/model/album_count_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_statistics_response_dto.dart
lib/model/person_update_dto.dart lib/model/person_update_dto.dart
lib/model/queue_status_dto.dart lib/model/queue_status_dto.dart
lib/model/reaction_type.dart
lib/model/recognition_config.dart lib/model/recognition_config.dart
lib/model/scan_library_dto.dart lib/model/scan_library_dto.dart
lib/model/search_album_response_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_dto.dart
lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_f_fmpeg_dto.dart
lib/model/system_config_job_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_machine_learning_dto.dart
lib/model/system_config_map_dto.dart lib/model/system_config_map_dto.dart
lib/model/system_config_new_version_check_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_tag_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart
lib/model/usage_by_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/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart lib/model/validate_access_token_response_dto.dart
lib/model/video_codec.dart lib/model/video_codec.dart
pubspec.yaml 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/add_users_dto_test.dart
test/admin_signup_response_dto_test.dart test/admin_signup_response_dto_test.dart
test/album_api_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_statistics_response_dto_test.dart
test/person_update_dto_test.dart test/person_update_dto_test.dart
test/queue_status_dto_test.dart test/queue_status_dto_test.dart
test/reaction_type_test.dart
test/recognition_config_test.dart test/recognition_config_test.dart
test/scan_library_dto_test.dart test/scan_library_dto_test.dart
test/search_album_response_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_dto_test.dart
test/system_config_f_fmpeg_dto_test.dart test/system_config_f_fmpeg_dto_test.dart
test/system_config_job_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_machine_learning_dto_test.dart
test/system_config_map_dto_test.dart test/system_config_map_dto_test.dart
test/system_config_new_version_check_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/update_user_dto_test.dart
test/usage_by_user_dto_test.dart test/usage_by_user_dto_test.dart
test/user_api_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/user_response_dto_test.dart
test/validate_access_token_response_dto_test.dart test/validate_access_token_response_dto_test.dart
test/video_codec_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: 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 - Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements ## Requirements
@@ -77,6 +77,10 @@ Class | Method | HTTP request | Description
*APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} | *APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} |
*APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key | *APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key |
*APIKeyApi* | [**updateKey**](doc//APIKeyApi.md#updatekey) | **PUT** /api-key/{id} | *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* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users | *AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album | *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* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} | *UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} |
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{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* | [**restoreUser**](doc//UserApi.md#restoreuser) | **POST** /user/{id}/restore |
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user | *UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user |
@@ -205,6 +208,9 @@ Class | Method | HTTP request | Description
- [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md)
- [APIKeyResponseDto](doc//APIKeyResponseDto.md) - [APIKeyResponseDto](doc//APIKeyResponseDto.md)
- [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md)
- [ActivityCreateDto](doc//ActivityCreateDto.md)
- [ActivityResponseDto](doc//ActivityResponseDto.md)
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md) - [AddUsersDto](doc//AddUsersDto.md)
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md) - [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
@@ -285,6 +291,7 @@ Class | Method | HTTP request | Description
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md) - [QueueStatusDto](doc//QueueStatusDto.md)
- [ReactionType](doc//ReactionType.md)
- [RecognitionConfig](doc//RecognitionConfig.md) - [RecognitionConfig](doc//RecognitionConfig.md)
- [ScanLibraryDto](doc//ScanLibraryDto.md) - [ScanLibraryDto](doc//ScanLibraryDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
@@ -312,6 +319,8 @@ Class | Method | HTTP request | Description
- [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
- [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md)
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md) - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
- [SystemConfigMapDto](doc//SystemConfigMapDto.md) - [SystemConfigMapDto](doc//SystemConfigMapDto.md)
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
@@ -338,7 +347,7 @@ Class | Method | HTTP request | Description
- [UpdateTagDto](doc//UpdateTagDto.md) - [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md) - [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md) - [UsageByUserDto](doc//UsageByUserDto.md)
- [UserCountResponseDto](doc//UserCountResponseDto.md) - [UserDto](doc//UserDto.md)
- [UserResponseDto](doc//UserResponseDto.md) - [UserResponseDto](doc//UserResponseDto.md)
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md) - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
- [VideoCodec](doc//VideoCodec.md) - [VideoCodec](doc//VideoCodec.md)

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

@@ -0,0 +1,244 @@
# openapi.api.ActivityApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**createActivity**](ActivityApi.md#createactivity) | **POST** /activity |
[**deleteActivity**](ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
[**getActivities**](ActivityApi.md#getactivities) | **GET** /activity |
[**getActivityStatistics**](ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
# **createActivity**
> ActivityResponseDto createActivity(activityCreateDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final activityCreateDto = ActivityCreateDto(); // ActivityCreateDto |
try {
final result = api_instance.createActivity(activityCreateDto);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->createActivity: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**activityCreateDto** | [**ActivityCreateDto**](ActivityCreateDto.md)| |
### Return type
[**ActivityResponseDto**](ActivityResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteActivity**
> deleteActivity(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.deleteActivity(id);
} catch (e) {
print('Exception when calling ActivityApi->deleteActivity: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getActivities**
> List<ActivityResponseDto> getActivities(albumId, assetId, type, userId)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final type = ; // ReactionType |
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getActivities(albumId, assetId, type, userId);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->getActivities: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**assetId** | **String**| | [optional]
**type** | [**ReactionType**](.md)| | [optional]
**userId** | **String**| | [optional]
### Return type
[**List<ActivityResponseDto>**](ActivityResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getActivityStatistics**
> ActivityStatisticsResponseDto getActivityStatistics(albumId, assetId)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getActivityStatistics(albumId, assetId);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->getActivityStatistics: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**assetId** | **String**| | [optional]
### Return type
[**ActivityStatisticsResponseDto**](ActivityStatisticsResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

18
mobile/openapi/doc/ActivityCreateDto.md generated Normal file
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 ## Load the model package
```dart ```dart
@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes 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) [[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) [[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** # **getMySharedLink**
> SharedLinkResponseDto getMySharedLink(key) > SharedLinkResponseDto getMySharedLink(password, token, key)
@@ -208,10 +208,12 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SharedLinkApi(); final api_instance = SharedLinkApi();
final password = password; // String |
final token = token_example; // String |
final key = key_example; // String | final key = key_example; // String |
try { try {
final result = api_instance.getMySharedLink(key); final result = api_instance.getMySharedLink(password, token, key);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling SharedLinkApi->getMySharedLink: $e\n'); print('Exception when calling SharedLinkApi->getMySharedLink: $e\n');
@@ -222,6 +224,8 @@ try {
Name | Type | Description | Notes Name | Type | Description | Notes
------------- | ------------- | ------------- | ------------- ------------- | ------------- | ------------- | -------------
**password** | **String**| | [optional]
**token** | **String**| | [optional]
**key** | **String**| | [optional] **key** | **String**| | [optional]
### Return type ### Return type

View File

@@ -14,6 +14,7 @@ Name | Type | Description | Notes
**assetIds** | **List<String>** | | [optional] [default to const []] **assetIds** | **List<String>** | | [optional] [default to const []]
**description** | **String** | | [optional] **description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**password** | **String** | | [optional]
**showMetadata** | **bool** | | [optional] [default to true] **showMetadata** | **bool** | | [optional] [default to true]
**type** | [**SharedLinkType**](SharedLinkType.md) | | **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] **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] **description** | **String** | | [optional]
**expiresAt** | [**DateTime**](DateTime.md) | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional]
**password** | **String** | | [optional]
**showMetadata** | **bool** | | [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) [[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) | | **expiresAt** | [**DateTime**](DateTime.md) | |
**id** | **String** | | **id** | **String** | |
**key** | **String** | | **key** | **String** | |
**password** | **String** | |
**showMetadata** | **bool** | | **showMetadata** | **bool** | |
**token** | **String** | | [optional]
**type** | [**SharedLinkType**](SharedLinkType.md) | | **type** | [**SharedLinkType**](SharedLinkType.md) | |
**userId** | **String** | | **userId** | **String** | |

View File

@@ -10,6 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | |
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | |
**library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | |
**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | | **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | |
**map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | | **map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | |
**newVersionCheck** | [**SystemConfigNewVersionCheckDto**](SystemConfigNewVersionCheckDto.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 | [**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} | [**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} |
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{id} | [**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{id} |
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count |
[**restoreUser**](UserApi.md#restoreuser) | **POST** /user/{id}/restore | [**restoreUser**](UserApi.md#restoreuser) | **POST** /user/{id}/restore |
[**updateUser**](UserApi.md#updateuser) | **PUT** /user | [**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) [[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** # **restoreUser**
> UserResponseDto restoreUser(id) > 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 'auth/http_bearer_auth.dart';
part 'api/api_key_api.dart'; part 'api/api_key_api.dart';
part 'api/activity_api.dart';
part 'api/album_api.dart'; part 'api/album_api.dart';
part 'api/asset_api.dart'; part 'api/asset_api.dart';
part 'api/audit_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_create_response_dto.dart';
part 'model/api_key_response_dto.dart'; part 'model/api_key_response_dto.dart';
part 'model/api_key_update_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/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart'; part 'model/admin_signup_response_dto.dart';
part 'model/album_count_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_statistics_response_dto.dart';
part 'model/person_update_dto.dart'; part 'model/person_update_dto.dart';
part 'model/queue_status_dto.dart'; part 'model/queue_status_dto.dart';
part 'model/reaction_type.dart';
part 'model/recognition_config.dart'; part 'model/recognition_config.dart';
part 'model/scan_library_dto.dart'; part 'model/scan_library_dto.dart';
part 'model/search_album_response_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_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_job_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_machine_learning_dto.dart';
part 'model/system_config_map_dto.dart'; part 'model/system_config_map_dto.dart';
part 'model/system_config_new_version_check_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_tag_dto.dart';
part 'model/update_user_dto.dart'; part 'model/update_user_dto.dart';
part 'model/usage_by_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/user_response_dto.dart';
part 'model/validate_access_token_response_dto.dart'; part 'model/validate_access_token_response_dto.dart';
part 'model/video_codec.dart'; part 'model/video_codec.dart';

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

@@ -0,0 +1,234 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityApi {
ActivityApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'POST /activity' operation and returns the [Response].
/// Parameters:
///
/// * [ActivityCreateDto] activityCreateDto (required):
Future<Response> createActivityWithHttpInfo(ActivityCreateDto activityCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/activity';
// ignore: prefer_final_locals
Object? postBody = activityCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [ActivityCreateDto] activityCreateDto (required):
Future<ActivityResponseDto?> createActivity(ActivityCreateDto activityCreateDto,) async {
final response = await createActivityWithHttpInfo(activityCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityResponseDto',) as ActivityResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /activity/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteActivityWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/activity/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteActivity(String id,) async {
final response = await deleteActivityWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /activity' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
///
/// * [ReactionType] type:
///
/// * [String] userId:
Future<Response> getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, String? userId, }) async {
// ignore: prefer_const_declarations
final path = r'/activity';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'albumId', albumId));
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
///
/// * [ReactionType] type:
///
/// * [String] userId:
Future<List<ActivityResponseDto>?> getActivities(String albumId, { String? assetId, ReactionType? type, String? userId, }) async {
final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, userId: userId, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<ActivityResponseDto>') as List)
.cast<ActivityResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /activity/statistics' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
Future<Response> getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async {
// ignore: prefer_const_declarations
final path = r'/activity/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'albumId', albumId));
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
Future<ActivityStatisticsResponseDto?> getActivityStatistics(String albumId, { String? assetId, }) async {
final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityStatisticsResponseDto',) as ActivityStatisticsResponseDto;
}
return null;
}
}

View File

@@ -173,8 +173,12 @@ class SharedLinkApi {
/// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response]. /// Performs an HTTP 'GET /shared-link/me' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] password:
///
/// * [String] token:
///
/// * [String] key: /// * [String] key:
Future<Response> getMySharedLinkWithHttpInfo({ String? key, }) async { Future<Response> getMySharedLinkWithHttpInfo({ String? password, String? token, String? key, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/shared-link/me'; final path = r'/shared-link/me';
@@ -185,6 +189,12 @@ class SharedLinkApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <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) { if (key != null) {
queryParams.addAll(_queryParams('', 'key', key)); queryParams.addAll(_queryParams('', 'key', key));
} }
@@ -205,9 +215,13 @@ class SharedLinkApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] password:
///
/// * [String] token:
///
/// * [String] key: /// * [String] key:
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, }) async { Future<SharedLinkResponseDto?> getMySharedLink({ String? password, String? token, String? key, }) async {
final response = await getMySharedLinkWithHttpInfo( key: key, ); final response = await getMySharedLinkWithHttpInfo( password: password, token: token, key: key, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

@@ -357,57 +357,6 @@ class UserApi {
return null; 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]. /// Performs an HTTP 'POST /user/{id}/restore' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@@ -189,6 +189,12 @@ class ApiClient {
return APIKeyResponseDto.fromJson(value); return APIKeyResponseDto.fromJson(value);
case 'APIKeyUpdateDto': case 'APIKeyUpdateDto':
return APIKeyUpdateDto.fromJson(value); return APIKeyUpdateDto.fromJson(value);
case 'ActivityCreateDto':
return ActivityCreateDto.fromJson(value);
case 'ActivityResponseDto':
return ActivityResponseDto.fromJson(value);
case 'ActivityStatisticsResponseDto':
return ActivityStatisticsResponseDto.fromJson(value);
case 'AddUsersDto': case 'AddUsersDto':
return AddUsersDto.fromJson(value); return AddUsersDto.fromJson(value);
case 'AdminSignupResponseDto': case 'AdminSignupResponseDto':
@@ -349,6 +355,8 @@ class ApiClient {
return PersonUpdateDto.fromJson(value); return PersonUpdateDto.fromJson(value);
case 'QueueStatusDto': case 'QueueStatusDto':
return QueueStatusDto.fromJson(value); return QueueStatusDto.fromJson(value);
case 'ReactionType':
return ReactionTypeTypeTransformer().decode(value);
case 'RecognitionConfig': case 'RecognitionConfig':
return RecognitionConfig.fromJson(value); return RecognitionConfig.fromJson(value);
case 'ScanLibraryDto': case 'ScanLibraryDto':
@@ -403,6 +411,10 @@ class ApiClient {
return SystemConfigFFmpegDto.fromJson(value); return SystemConfigFFmpegDto.fromJson(value);
case 'SystemConfigJobDto': case 'SystemConfigJobDto':
return SystemConfigJobDto.fromJson(value); return SystemConfigJobDto.fromJson(value);
case 'SystemConfigLibraryDto':
return SystemConfigLibraryDto.fromJson(value);
case 'SystemConfigLibraryScanDto':
return SystemConfigLibraryScanDto.fromJson(value);
case 'SystemConfigMachineLearningDto': case 'SystemConfigMachineLearningDto':
return SystemConfigMachineLearningDto.fromJson(value); return SystemConfigMachineLearningDto.fromJson(value);
case 'SystemConfigMapDto': case 'SystemConfigMapDto':
@@ -455,8 +467,8 @@ class ApiClient {
return UpdateUserDto.fromJson(value); return UpdateUserDto.fromJson(value);
case 'UsageByUserDto': case 'UsageByUserDto':
return UsageByUserDto.fromJson(value); return UsageByUserDto.fromJson(value);
case 'UserCountResponseDto': case 'UserDto':
return UserCountResponseDto.fromJson(value); return UserDto.fromJson(value);
case 'UserResponseDto': case 'UserResponseDto':
return UserResponseDto.fromJson(value); return UserResponseDto.fromJson(value);
case 'ValidateAccessTokenResponseDto': case 'ValidateAccessTokenResponseDto':

View File

@@ -97,6 +97,9 @@ String parameterToString(dynamic value) {
if (value is PathType) { if (value is PathType) {
return PathTypeTypeTransformer().encode(value).toString(); return PathTypeTypeTransformer().encode(value).toString();
} }
if (value is ReactionType) {
return ReactionTypeTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) { if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString(); return SharedLinkTypeTypeTransformer().encode(value).toString();
} }

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