Compare commits

..

5 commits

Author SHA1 Message Date
Fynn Petersen-Frey
de7475f3fc prettier 2023-11-29 20:57:59 +01:00
Fynn Petersen-Frey
7f5d554d1a fix tests 2023-11-29 20:38:13 +01:00
Fynn Petersen-Frey
e033e9aad2 timeOfRequest -> requestedAt 2023-11-29 19:46:21 +01:00
Fynn Petersen-Frey
061c567468 prettier 2023-11-29 17:17:44 +01:00
Fynn Petersen-Frey
4ce3676127 fix(mobile): sync issue due to time mismatch 2023-11-29 17:08:03 +01:00
253 changed files with 1266 additions and 11234 deletions

2
.gitattributes vendored
View file

@ -17,5 +17,3 @@ web/src/api/open-api/**/*.md -diff -merge
web/src/api/open-api/**/*.md linguist-generated=true
web/src/api/open-api/**/*.ts -diff -merge
web/src/api/open-api/**/*.ts linguist-generated=true
*.sh text eol=lf

View file

@ -35,7 +35,7 @@ jobs:
with:
ref: ${{ steps.get-ref.outputs.ref }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "12.x"

View file

@ -209,11 +209,11 @@ jobs:
exit 1
generated-typeorm-migrations-up-to-date:
name: TypeORM Checks
name: TypeORM Migrations
runs-on: ubuntu-latest
services:
postgres:
image: postgres@sha256:6dfee32131933ab4ca25a00360c3f427fdc134de56f9a90c6c9a4956b48aea85
image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
@ -236,7 +236,7 @@ jobs:
- name: Install server dependencies
run: npm ci
- name: Build the app
- name: Build the
run: npm run build
- name: Run existing migrations
@ -252,30 +252,13 @@ jobs:
with:
files: |
server/src/infra/migrations/
- name: Verify migration files have not changed
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated migration files not up to date!"
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1
- name: Run SQL generation
run: npm run sql:generate
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-sql-files
with:
files: |
server/src/infra/sql
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated SQL files not up to date!"
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
exit 1
# mobile-integration-tests:
# name: Run mobile end-to-end integration tests
# runs-on: macos-latest

View file

@ -26,10 +26,7 @@ prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
api:
npm --prefix server run api:generate
sql:
npm --prefix server run sql:generate
cd ./server && npm run api:generate
attach-server:
docker exec -it docker_immich-server_1 sh

View file

@ -11,7 +11,7 @@
<p align="center">
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
</p>
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos</h3>
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup Lösung für Fotos und Videos</h3>
<br/>
<a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Haupt-Screenshot">
@ -32,10 +32,10 @@
## Warnung
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
- ⚠️ Das Projekt befindet sich unter **sehr aktiver** Entwicklung.
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup Regel für deine wertvollen Fotos und Videos!
## Inhalt
@ -54,9 +54,9 @@ Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://im
## Demo
Die Web-Demo kannst Du unter https://demo.immich.app finden.
Die Web-Demo kannst du unter https://demo.immich.app finden.
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
Für die Handy-App kannst du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
```bash title="Demo Credential"
Die Anmeldedaten
@ -73,8 +73,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Funktionen | Mobil | Web |
| ---------------------------------------------------- | ------ | ----- |
| Fotos & Videos hochladen und ansehen | Ja | Ja |
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. |
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. |
| Automatisches Backup wenn die App offen ist | Ja | n. a. |
| Selektive Auswahl von Alben zum Backup | Ja | n. a. |
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
| Unterstützt mehrere Benutzer | Ja | Ja |
| Album und geteilte Alben | Ja | Ja |
@ -82,7 +82,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Unterstützt RAW Formate | Ja | Ja |
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja |
| Administrative Funktionen (Nutzerverwaltung) | Nein | Ja |
| Backup im Hintergrund | Ja | n. a. |
| Virtuelles Scrollen | Ja | Ja |
| OAuth Unterstützung | Ja | Ja |
@ -101,22 +101,22 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
## Unterstütze das Projekt
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung, um mir zusätzliche Motivation zu geben, weiterzumachen.
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung mir zusätzliche Motivation zu geben um weiterzumachen.
Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach, das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
### Spenden
- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors
- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors
- [Monatliche Spende](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Einmalige Spende](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
## Mitwirkende
## Unterstützer
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>

12
cli/package-lock.json generated
View file

@ -1604,9 +1604,9 @@
}
},
"node_modules/@types/node": {
"version": "20.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
"version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@ -7990,9 +7990,9 @@
}
},
"@types/node": {
"version": "20.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
"version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"

View file

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.89.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -447,12 +447,6 @@ export interface AssetBulkDeleteDto {
* @interface AssetBulkUpdateDto
*/
export interface AssetBulkUpdateDto {
/**
*
* @type {string}
* @memberof AssetBulkUpdateDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {Array<string>}
@ -471,18 +465,6 @@ export interface AssetBulkUpdateDto {
* @memberof AssetBulkUpdateDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'longitude'?: number;
/**
*
* @type {boolean}
@ -586,142 +568,6 @@ export const AssetBulkUploadCheckResultReasonEnum = {
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
/**
*
* @export
* @interface AssetFaceResponseDto
*/
export interface AssetFaceResponseDto {
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'boundingBoxX1': number;
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'boundingBoxX2': number;
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'boundingBoxY1': number;
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'boundingBoxY2': number;
/**
*
* @type {string}
* @memberof AssetFaceResponseDto
*/
'id': string;
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'imageHeight': number;
/**
*
* @type {number}
* @memberof AssetFaceResponseDto
*/
'imageWidth': number;
/**
*
* @type {PersonResponseDto}
* @memberof AssetFaceResponseDto
*/
'person': PersonResponseDto | null;
}
/**
*
* @export
* @interface AssetFaceUpdateDto
*/
export interface AssetFaceUpdateDto {
/**
*
* @type {Array<AssetFaceUpdateItem>}
* @memberof AssetFaceUpdateDto
*/
'data': Array<AssetFaceUpdateItem>;
}
/**
*
* @export
* @interface AssetFaceUpdateItem
*/
export interface AssetFaceUpdateItem {
/**
*
* @type {string}
* @memberof AssetFaceUpdateItem
*/
'assetId': string;
/**
*
* @type {string}
* @memberof AssetFaceUpdateItem
*/
'personId': string;
}
/**
*
* @export
* @interface AssetFaceWithoutPersonResponseDto
*/
export interface AssetFaceWithoutPersonResponseDto {
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'boundingBoxX1': number;
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'boundingBoxX2': number;
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'boundingBoxY1': number;
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'boundingBoxY2': number;
/**
*
* @type {string}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'id': string;
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'imageHeight': number;
/**
*
* @type {number}
* @memberof AssetFaceWithoutPersonResponseDto
*/
'imageWidth': number;
}
/**
*
* @export
@ -978,10 +824,10 @@ export interface AssetResponseDto {
'ownerId': string;
/**
*
* @type {Array<PersonWithFacesResponseDto>}
* @type {Array<PersonResponseDto>}
* @memberof AssetResponseDto
*/
'people'?: Array<PersonWithFacesResponseDto>;
'people'?: Array<PersonResponseDto>;
/**
*
* @type {boolean}
@ -1113,6 +959,12 @@ export interface AuditDeletesResponseDto {
* @memberof AuditDeletesResponseDto
*/
'needsFullSync': boolean;
/**
*
* @type {string}
* @memberof AuditDeletesResponseDto
*/
'requestedAt'?: string;
}
/**
*
@ -1808,19 +1660,6 @@ export interface ExifResponseDto {
*/
'timeZone'?: string | null;
}
/**
*
* @export
* @interface FaceDto
*/
export interface FaceDto {
/**
*
* @type {string}
* @memberof FaceDto
*/
'id': string;
}
/**
*
* @export
@ -1934,8 +1773,7 @@ export const JobCommand = {
Start: 'start',
Pause: 'pause',
Resume: 'resume',
Empty: 'empty',
ClearFailed: 'clear-failed'
Empty: 'empty'
} as const;
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
@ -2713,49 +2551,6 @@ export interface PersonUpdateDto {
*/
'name'?: string;
}
/**
*
* @export
* @interface PersonWithFacesResponseDto
*/
export interface PersonWithFacesResponseDto {
/**
*
* @type {string}
* @memberof PersonWithFacesResponseDto
*/
'birthDate': string | null;
/**
*
* @type {Array<AssetFaceWithoutPersonResponseDto>}
* @memberof PersonWithFacesResponseDto
*/
'faces': Array<AssetFaceWithoutPersonResponseDto>;
/**
*
* @type {string}
* @memberof PersonWithFacesResponseDto
*/
'id': string;
/**
*
* @type {boolean}
* @memberof PersonWithFacesResponseDto
*/
'isHidden': boolean;
/**
*
* @type {string}
* @memberof PersonWithFacesResponseDto
*/
'name': string;
/**
*
* @type {string}
* @memberof PersonWithFacesResponseDto
*/
'thumbnailPath': string;
}
/**
*
* @export
@ -4348,12 +4143,6 @@ export interface UpdateAlbumDto {
* @interface UpdateAssetDto
*/
export interface UpdateAssetDto {
/**
*
* @type {string}
* @memberof UpdateAssetDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {string}
@ -4372,18 +4161,6 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'longitude'?: number;
}
/**
*
@ -11541,233 +11318,6 @@ export class AuthenticationApi extends BaseAPI {
}
/**
* FaceApi - axios parameter creator
* @export
*/
export const FaceApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getFaces', 'id', id)
const localVarPath = `/face`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (id !== undefined) {
localVarQueryParameter['id'] = id;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {FaceDto} faceDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('reassignFacesById', 'id', id)
// verify required parameter 'faceDto' is not null or undefined
assertParamExists('reassignFacesById', 'faceDto', faceDto)
const localVarPath = `/face/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* FaceApi - functional programming interface
* @export
*/
export const FaceApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration)
return {
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetFaceResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {FaceDto} faceDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* FaceApi - factory interface
* @export
*/
export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = FaceApiFp(configuration)
return {
/**
*
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetFaceResponseDto>> {
return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for getFaces operation in FaceApi.
* @export
* @interface FaceApiGetFacesRequest
*/
export interface FaceApiGetFacesRequest {
/**
*
* @type {string}
* @memberof FaceApiGetFaces
*/
readonly id: string
}
/**
* Request parameters for reassignFacesById operation in FaceApi.
* @export
* @interface FaceApiReassignFacesByIdRequest
*/
export interface FaceApiReassignFacesByIdRequest {
/**
*
* @type {string}
* @memberof FaceApiReassignFacesById
*/
readonly id: string
/**
*
* @type {FaceDto}
* @memberof FaceApiReassignFacesById
*/
readonly faceDto: FaceDto
}
/**
* FaceApi - object-oriented interface
* @export
* @class FaceApi
* @extends {BaseAPI}
*/
export class FaceApi extends BaseAPI {
/**
*
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FaceApi
*/
public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) {
return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FaceApi
*/
public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) {
return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath));
}
}
/**
* JobApi - axios parameter creator
* @export
@ -13599,44 +13149,6 @@ export class PartnerApi extends BaseAPI {
*/
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/person`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {boolean} [withHidden]
@ -13896,54 +13408,6 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('reassignFaces', 'id', id)
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
const localVarPath = `/person/{id}/reassign`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -14046,15 +13510,6 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {boolean} [withHidden]
@ -14116,17 +13571,6 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -14158,14 +13602,6 @@ export const PersonApiFp = function(configuration?: Configuration) {
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = PersonApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson(options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
return localVarFp.createPerson(options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -14220,15 +13656,6 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -14341,27 +13768,6 @@ export interface PersonApiMergePersonRequest {
readonly mergePersonDto: MergePersonDto
}
/**
* Request parameters for reassignFaces operation in PersonApi.
* @export
* @interface PersonApiReassignFacesRequest
*/
export interface PersonApiReassignFacesRequest {
/**
*
* @type {string}
* @memberof PersonApiReassignFaces
*/
readonly id: string
/**
*
* @type {AssetFaceUpdateDto}
* @memberof PersonApiReassignFaces
*/
readonly assetFaceUpdateDto: AssetFaceUpdateDto
}
/**
* Request parameters for updatePeople operation in PersonApi.
* @export
@ -14404,16 +13810,6 @@ export interface PersonApiUpdatePersonRequest {
* @extends {BaseAPI}
*/
export class PersonApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public createPerson(options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -14480,17 +13876,6 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.89.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.89.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.89.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.89.0
* The version of the OpenAPI document: 1.88.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -108,11 +108,11 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
env_file:
- .env
environment:

View file

@ -65,12 +65,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
env_file:
- .env
environment:

View file

@ -23,7 +23,7 @@ services:
- database
database:
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
command: -c fsync=off
environment:
POSTGRES_PASSWORD: postgres

View file

@ -69,12 +69,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
env_file:
- .env
environment:

View file

@ -144,7 +144,7 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`.
```bash title="Start the containers using docker compose command"
docker compose up -d
docker-compose up -d # or `docker compose up -d` based on your docker-compose version
```
:::tip
@ -162,7 +162,7 @@ If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
```bash title="Upgrade Immich"
docker compose pull && docker compose up -d
docker-compose pull && docker-compose up -d # Or `docker compose up -d`
```
:::caution Automatic Updates

View file

@ -1,4 +1,4 @@
FROM python:3.11-bookworm@sha256:ba7a7ac30c38e119c4304f98ef0e188f90f4f67a958bb6899da9defb99bfb471 as builder
FROM python:3.11-bookworm@sha256:e5a1b0a194a5fbf94f6e350b31c9a508723f9eeb2f9e9e32c3b65df8520a40cc as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
FROM python:3.11-slim-bookworm@sha256:cc758519481092eb5a4a5ab0c1b303e288880d59afc601958d19e95b300bc86b
FROM python:3.11-slim-bookworm@sha256:1bc6a3e9356d64ea632791653bc71a56340e8741dab66434ab2739ebf6aed29d
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*

View file

@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:e296d47be09fc5d260eba9b191f60496f028a4f3ec41e8a14d48c0bae2c60244 as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.89.0"
version = "1.88.2"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View file

@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 113,
"android.injected.version.name" => "1.89.0",
"android.injected.version.code" => 112,
"android.injected.version.name" => "1.88.2",
}
)
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

@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2

View file

@ -144,8 +144,6 @@
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_upload": "Upload",
"control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_edit_location": "Edit Location",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Share",
@ -167,7 +165,6 @@
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_location_add": "Add a location",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
@ -464,18 +461,5 @@
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack",
"scaffold_body_error_occurred": "Error occurred",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"edit_location_dialog_title": "Location",
"map_location_picker_page_use_location": "Use this location",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_hint": "Enter your latitude here",
"location_picker_latitude_error": "Enter a valid latitude",
"location_picker_longitude": "Longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"location_picker_longitude_error": "Enter a valid longitude"
"scaffold_body_error_occured": "Error occured"
}

View file

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

View file

@ -1,36 +0,0 @@
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:timezone/timezone.dart';
extension TZExtension on Asset {
/// Returns the created time of the asset from the exif info (if available) or from
/// the fileCreatedAt field, adjusted to the timezone value from the exif info along with
/// the timezone offset in [Duration]
(DateTime, Duration) getTZAdjustedTimeAndOffset() {
DateTime dt = fileCreatedAt.toLocal();
if (exifInfo?.dateTimeOriginal != null) {
dt = exifInfo!.dateTimeOriginal!;
if (exifInfo?.timeZone != null) {
dt = dt.toUtc();
try {
final location = getLocation(exifInfo!.timeZone!);
dt = TZDateTime.from(dt, location);
} on LocationNotFoundException {
RegExp re = RegExp(
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
caseSensitive: false,
);
final m = re.firstMatch(exifInfo!.timeZone!);
if (m != null) {
final duration = Duration(
hours: int.parse(m.group(1) ?? '0'),
minutes: int.parse(m.group(2) ?? '0'),
);
dt = dt.add(duration);
return (dt, duration);
}
}
}
}
return (dt, dt.timeZoneOffset);
}
}

View file

@ -1,4 +0,0 @@
extension TZOffsetExtension on Duration {
String formatAsOffset() =>
"${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
}

View file

@ -95,11 +95,7 @@ class ActivityStatisticsNotifier extends StateNotifier<int> {
}
Future<void> fetchStatistics() async {
final count =
await _activityService.getStatistics(albumId, assetId: assetId);
if (mounted) {
state = count;
}
state = await _activityService.getStatistics(albumId, assetId: assetId);
}
Future<void> addActivity() async {

View file

@ -68,20 +68,21 @@ class AlbumThumbnailListTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: BorderRadius.circular(8),
child: album.thumbnail.value == null
? buildEmptyThumbnail()
: buildAlbumThumbnail(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
@ -109,7 +110,6 @@ class AlbumThumbnailListTile extends StatelessWidget {
],
),
),
),
],
),
),

View file

@ -135,24 +135,18 @@ class LibraryPage extends HookConsumerWidget {
}
Widget buildCreateAlbumButton() {
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
return GestureDetector(
onTap: () {
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
},
child: Padding(
padding:
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: cardSize,
height: cardSize,
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: isDarkTheme
@ -170,6 +164,7 @@ class LibraryPage extends HookConsumerWidget {
),
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 8.0,
@ -184,8 +179,6 @@ class LibraryPage extends HookConsumerWidget {
),
),
);
},
);
}
Widget buildLibraryNavButton(

View file

@ -4,15 +4,14 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:timezone/timezone.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:url_launcher/url_launcher.dart';
@ -22,30 +21,53 @@ class ExifBottomSheet extends HookConsumerWidget {
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
bool hasCoordinates() =>
bool hasCoordinates(ExifInfo? exifInfo) =>
exifInfo != null &&
exifInfo.latitude != null &&
exifInfo.longitude != null &&
exifInfo.latitude != 0 &&
exifInfo.longitude != 0;
String formattedDateTime() {
final (dt, timeZone) =
(assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset();
final date = DateFormat.yMMMEd().format(dt);
final time = DateFormat.jm().format(dt);
String formatTimeZone(Duration d) =>
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
return '$date$time GMT${timeZone.formatAsOffset()}';
String get formattedDateTime {
DateTime dt = asset.fileCreatedAt.toLocal();
String? timeZone;
if (asset.exifInfo?.dateTimeOriginal != null) {
dt = asset.exifInfo!.dateTimeOriginal!;
if (asset.exifInfo?.timeZone != null) {
dt = dt.toUtc();
try {
final location = getLocation(asset.exifInfo!.timeZone!);
dt = TZDateTime.from(dt, location);
} on LocationNotFoundException {
RegExp re = RegExp(
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
caseSensitive: false,
);
final m = re.firstMatch(asset.exifInfo!.timeZone!);
if (m != null) {
final duration = Duration(
hours: int.parse(m.group(1) ?? '0'),
minutes: int.parse(m.group(2) ?? '0'),
);
dt = dt.add(duration);
timeZone = formatTimeZone(duration);
}
}
}
}
Future<Uri?> createCoordinatesUri() async {
if (!hasCoordinates()) {
final date = DateFormat.yMMMEd().format(dt);
final time = DateFormat.jm().format(dt);
timeZone ??= formatTimeZone(dt.timeZoneOffset);
return '$date$time $timeZone';
}
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
if (!hasCoordinates(exifInfo)) {
return null;
}
@ -86,20 +108,24 @@ class ExifBottomSheet extends HookConsumerWidget {
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
buildMap() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: LayoutBuilder(
builder: (context, constraints) {
return MapThumbnail(
showAttribution: false,
coords: LatLng(
exifInfo?.latitude ?? 0,
exifInfo?.longitude ?? 0,
),
height: 150,
width: constraints.maxWidth,
zoom: 12.0,
zoom: 16.0,
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
@ -113,7 +139,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
],
onTap: (tapPosition, latLong) async {
Uri? uri = await createCoordinatesUri();
Uri? uri = await _createCoordinatesUri(exifInfo);
if (uri == null) {
return;
@ -155,26 +181,8 @@ class ExifBottomSheet extends HookConsumerWidget {
buildLocation() {
// Guard no lat/lng
if (!hasCoordinates()) {
return asset.isRemote
? ListTile(
minLeadingWidth: 0,
contentPadding: const EdgeInsets.all(0),
leading: const Icon(Icons.location_on),
title: Text(
"exif_bottom_sheet_location_add",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
onTap: () => handleEditLocation(
ref,
context,
[assetWithExif.value ?? asset],
),
)
: const SizedBox.shrink();
if (!hasCoordinates(exifInfo)) {
return Container();
}
return Column(
@ -182,30 +190,14 @@ class ExifBottomSheet extends HookConsumerWidget {
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"exif_bottom_sheet_location",
style: context.textTheme.labelMedium?.copyWith(
color:
context.textTheme.labelMedium?.color?.withAlpha(200),
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
if (asset.isRemote)
IconButton(
onPressed: () => handleEditLocation(
ref,
context,
[assetWithExif.value ?? asset],
),
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
),
buildMap(),
RichText(
text: TextSpan(
@ -241,27 +233,12 @@ class ExifBottomSheet extends HookConsumerWidget {
}
buildDate() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
formattedDateTime(),
return Text(
formattedDateTime,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (asset.isRemote)
IconButton(
onPressed: () => handleEditDateTime(
ref,
context,
[assetWithExif.value ?? asset],
),
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
);
}
@ -386,7 +363,7 @@ class ExifBottomSheet extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: hasCoordinates() ? 5 : 0,
flex: hasCoordinates(exifInfo) ? 5 : 0,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: buildLocation(),
@ -425,8 +402,9 @@ class ExifBottomSheet extends HookConsumerWidget {
child: CircularProgressIndicator.adaptive(),
),
),
const SizedBox(height: 8.0),
buildLocation(),
SizedBox(height: hasCoordinates() ? 16.0 : 6.0),
SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),
buildDetail(),
const SizedBox(height: 50),
],

View file

@ -795,7 +795,6 @@ class GalleryViewerPage extends HookConsumerWidget {
tag: isFromDto
? '${a.remoteId}-$heroOffset'
: a.id + heroOffset,
transitionOnUserGestures: true,
),
filterQuality: FilterQuality.high,
tightMode: true,

View file

@ -375,8 +375,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
await _getBackupAlbumsInfo();
await updateServerInfo();
await _updateBackupAssetCount();
} else {
log.warning("cannot get backup info - background backup is in progress!");
}
}

View file

@ -19,8 +19,6 @@ class ControlBottomAppBar extends ConsumerWidget {
final void Function() onCreateNewAlbum;
final void Function() onUpload;
final void Function() onStack;
final void Function() onEditTime;
final void Function() onEditLocation;
final List<Album> albums;
final List<Album> sharedAlbums;
@ -39,8 +37,6 @@ class ControlBottomAppBar extends ConsumerWidget {
required this.onCreateNewAlbum,
required this.onUpload,
required this.onStack,
required this.onEditTime,
required this.onEditLocation,
this.selectionAssetState = const SelectionAssetState(),
this.enabled = true,
}) : super(key: key);
@ -78,18 +74,6 @@ class ControlBottomAppBar extends ConsumerWidget {
label: "control_bottom_app_bar_favorite".tr(),
onPressed: enabled ? onFavorite : null,
),
if (hasRemote)
ControlBoxButton(
iconData: Icons.edit_calendar_outlined,
label: "control_bottom_app_bar_edit_time".tr(),
onPressed: enabled ? onEditTime : null,
),
if (hasRemote)
ControlBoxButton(
iconData: Icons.edit_location_alt_outlined,
label: "control_bottom_app_bar_edit_location".tr(),
onPressed: enabled ? onEditLocation : null,
),
ControlBoxButton(
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(),

View file

@ -312,34 +312,6 @@ class HomePage extends HookConsumerWidget {
}
}
void onEditTime() async {
try {
final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
);
if (remoteAssets.isNotEmpty) {
handleEditDateTime(ref, context, remoteAssets.toList());
}
} finally {
selectionEnabledHook.value = false;
}
}
void onEditLocation() async {
try {
final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
);
if (remoteAssets.isNotEmpty) {
handleEditLocation(ref, context, remoteAssets.toList());
}
} finally {
selectionEnabledHook.value = false;
}
}
Future<void> refreshAssets() async {
final fullRefresh = refreshCount.value > 0;
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
@ -439,8 +411,6 @@ class HomePage extends HookConsumerWidget {
enabled: !processing.value,
selectionAssetState: selectionAssetState.value,
onStack: onStack,
onEditTime: onEditTime,
onEditLocation: onEditLocation,
),
],
),

View file

@ -5,8 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/shared/models/user.dart';
@ -23,7 +21,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier(
this._apiService,
this._db,
this._ref,
) : super(
AuthenticationState(
deviceId: "",
@ -39,8 +36,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
final ApiService _apiService;
final Isar _db;
final StateNotifierProviderRef<AuthenticationNotifier, AuthenticationState>
_ref;
final _log = Logger("AuthenticationNotifier");
Future<bool> login(
@ -116,8 +111,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
]);
_ref.invalidate(albumProvider);
_ref.invalidate(sharedAlbumProvider);
state = state.copyWith(
deviceId: "",
@ -229,6 +222,5 @@ final authenticationProvider =
return AuthenticationNotifier(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref,
);
});

View file

@ -1,113 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:latlong2/latlong.dart';
class MapLocationPickerPage extends HookConsumerWidget {
final LatLng? initialLatLng;
const MapLocationPickerPage({super.key, this.initialLatLng});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedLatLng = useState<LatLng>(initialLatLng ?? LatLng(0, 0));
final isDarkTheme =
ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
final isLoading =
ref.watch(mapStateNotifier.select((state) => state.isLoading));
final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
return Theme(
// Override app theme based on map theme
data: isDarkTheme ? immichDarkTheme : immichLightTheme,
child: Scaffold(
extendBodyBehindAppBar: true,
body: Stack(
children: [
if (!isLoading)
FlutterMap(
options: MapOptions(
maxBounds:
LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
interactiveFlags: InteractiveFlag.doubleTapZoom |
InteractiveFlag.drag |
InteractiveFlag.flingAnimation |
InteractiveFlag.pinchMove |
InteractiveFlag.pinchZoom,
center: LatLng(20, 20),
zoom: 2,
minZoom: 1,
maxZoom: maxZoom,
onTap: (tapPosition, point) => selectedLatLng.value = point,
),
children: [
ref.read(mapStateNotifier.notifier).getTileLayer(),
MarkerLayer(
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: selectedLatLng.value,
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png'),
),
height: 40,
width: 40,
),
],
),
],
),
if (isLoading)
Positioned(
top: context.height * 0.35,
left: context.width * 0.425,
child: const ImmichLoadingIndicator(),
),
],
),
bottomSheet: BottomSheet(
onClosing: () {},
builder: (context) => SizedBox(
height: 150,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${selectedLatLng.value.latitude.toStringAsFixed(4)}, ${selectedLatLng.value.longitude.toStringAsFixed(4)}",
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
fontWeight: FontWeight.w600,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => context.autoPop(selectedLatLng.value),
child: const Text("map_location_picker_page_use_location")
.tr(),
),
ElevatedButton(
onPressed: () => context.autoPop(),
style: ElevatedButton.styleFrom(
backgroundColor: context.colorScheme.error,
),
child: const Text("action_common_cancel").tr(),
),
],
),
],
),
),
),
),
);
}
}

View file

@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart';
@ -14,15 +12,13 @@ class MapThumbnail extends HookConsumerWidget {
final double zoom;
final List<Marker> markers;
final double height;
final double width;
final bool showAttribution;
final bool isDarkTheme;
const MapThumbnail({
super.key,
required this.coords,
this.height = 100,
this.width = 100,
required this.height,
this.onTap,
this.zoom = 1,
this.showAttribution = true,
@ -32,33 +28,18 @@ class MapThumbnail extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final mapController = useMapController();
final isMapReady = useRef(false);
ref.watch(mapStateNotifier.select((s) => s.mapStyle));
useEffect(
() {
if (isMapReady.value && mapController.center != coords) {
mapController.move(coords, zoom);
}
return null;
},
[coords],
);
return SizedBox(
height: height,
width: width,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: FlutterMap(
mapController: mapController,
options: MapOptions(
interactiveFlags: InteractiveFlag.none,
center: coords,
zoom: zoom,
onTap: onTap,
onMapReady: () => isMapReady.value = true,
),
nonRotatedChildren: [
if (showAttribution)

View file

@ -1,32 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/flutter_map.dart';
MapController useMapController({
String? debugLabel,
List<Object?>? keys,
}) {
return use(_MapControllerHook(keys: keys));
}
class _MapControllerHook extends Hook<MapController> {
const _MapControllerHook({List<Object?>? keys}) : super(keys: keys);
@override
HookState<MapController, Hook<MapController>> createState() =>
_MapControllerHookState();
}
class _MapControllerHookState
extends HookState<MapController, _MapControllerHook> {
late final controller = MapController();
@override
MapController build(BuildContext context) => controller;
@override
void dispose() => controller.dispose();
@override
String get debugLabel => 'useMapController';
}

View file

@ -55,7 +55,6 @@ class MapPageState extends ConsumerState<MapPage> {
// in onMapEvent() since MapEventMove#id is not populated properly in the
// current version of flutter_map(4.0.0) used
bool forceAssetUpdate = false;
bool isMapReady = false;
late final Debounce debounce;
@override
@ -80,7 +79,7 @@ class MapPageState extends ConsumerState<MapPage> {
bool forceReload = false,
}) {
try {
final bounds = isMapReady ? mapController.bounds : null;
final bounds = mapController.bounds;
if (bounds != null) {
final oldAssetsInBounds = assetsInBounds.toSet();
assetsInBounds =
@ -456,7 +455,6 @@ class MapPageState extends ConsumerState<MapPage> {
minZoom: 1,
maxZoom: maxZoom,
onMapReady: () {
isMapReady = true;
mapController.mapEventStream.listen(onMapEvent);
},
),

View file

@ -6,7 +6,7 @@ part of 'person.service.dart';
// RiverpodGenerator
// **************************************************************************
String _$personServiceHash() => r'cde0a9c029d16ddde2adcd58ae8c863bf8cc1fed';
String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b';
/// See also [personService].
@ProviderFor(personService)

View file

@ -29,8 +29,9 @@ class CuratedPlacesRow extends CuratedRow {
onTap: () => context.autoPush(
const MapRoute(),
),
child: SizedBox.square(
dimension: imageSize,
child: SizedBox(
height: imageSize,
width: imageSize,
child: Stack(
children: [
Padding(
@ -42,7 +43,6 @@ class CuratedPlacesRow extends CuratedRow {
5,
),
height: imageSize,
width: imageSize,
showAttribution: false,
isDarkTheme: context.isDarkTheme,
),

View file

@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
// **************************************************************************
String _$appSettingsServiceHash() =>
r'45ea609a91d250290431a7a08a14d16b37c7515d';
r'957a65af6967701112f3076b507f9738fec4b7be';
/// See also [appSettingsService].
@ProviderFor(appSettingsService)

View file

@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
class CustomTransitionsBuilders {
const CustomTransitionsBuilders._();
static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder =
ZoomPageTransitionsBuilder();
static const RouteTransitionsBuilder zoomedPage = _zoomedPage;
static Widget _zoomedPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return zoomPageTransitionsBuilder.buildTransitions(
// Empty PageRoute<> object, only used to pass allowSnapshotting to ZoomPageTransitionsBuilder
PageRouteBuilder(
allowSnapshotting: true,
fullscreenDialog: false,
pageBuilder: (context, animation, secondaryAnimation) =>
const SizedBox.shrink(),
),
context,
animation,
secondaryAnimation,
child,
);
}
}

View file

@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
import 'package:immich_mobile/modules/album/views/library_page.dart';
import 'package:immich_mobile/modules/map/ui/map_location_picker.dart';
import 'package:immich_mobile/modules/map/views/map_page.dart';
import 'package:immich_mobile/modules/memories/models/memory.dart';
import 'package:immich_mobile/modules/memories/views/memory_page.dart';
@ -44,7 +43,6 @@ import 'package:immich_mobile/modules/search/views/search_page.dart';
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
import 'package:immich_mobile/routing/custom_transition_builders.dart';
import 'package:immich_mobile/routing/duplicate_guard.dart';
import 'package:immich_mobile/routing/backup_permission_guard.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@ -58,8 +56,7 @@ import 'package:immich_mobile/shared/views/app_log_page.dart';
import 'package:immich_mobile/shared/views/splash_screen.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
import 'package:isar/isar.dart';
import 'package:photo_manager/photo_manager.dart' hide LatLng;
import 'package:latlong2/latlong.dart';
import 'package:photo_manager/photo_manager.dart';
part 'router.gr.dart';
@ -89,10 +86,9 @@ part 'router.gr.dart';
],
transitionsBuilder: TransitionsBuilders.fadeIn,
),
CustomRoute(
AutoRoute(
page: GalleryViewerPage,
guards: [AuthGuard, DuplicateGuard],
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute(
@ -174,10 +170,6 @@ part 'router.gr.dart';
transitionsBuilder: TransitionsBuilders.slideLeft,
durationInMilliseconds: 200,
),
CustomRoute<LatLng?>(
page: MapLocationPickerPage,
guards: [AuthGuard, DuplicateGuard],
),
],
)
class AppRouter extends _$AppRouter {

View file

@ -63,7 +63,7 @@ class _$AppRouter extends RootStackRouter {
},
GalleryViewerRoute.name: (routeData) {
final args = routeData.argsAs<GalleryViewerRouteArgs>();
return CustomPage<dynamic>(
return MaterialPageX<dynamic>(
routeData: routeData,
child: GalleryViewerPage(
key: args.key,
@ -75,9 +75,6 @@ class _$AppRouter extends RootStackRouter {
isOwner: args.isOwner,
sharedAlbumId: args.sharedAlbumId,
),
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
opaque: true,
barrierDismissible: false,
);
},
VideoViewerRoute.name: (routeData) {
@ -360,19 +357,6 @@ class _$AppRouter extends RootStackRouter {
barrierDismissible: false,
);
},
MapLocationPickerRoute.name: (routeData) {
final args = routeData.argsAs<MapLocationPickerRouteArgs>(
orElse: () => const MapLocationPickerRouteArgs());
return CustomPage<LatLng?>(
routeData: routeData,
child: MapLocationPickerPage(
key: args.key,
initialLatLng: args.initialLatLng,
),
opaque: true,
barrierDismissible: false,
);
},
HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
@ -717,14 +701,6 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard,
],
),
RouteConfig(
MapLocationPickerRoute.name,
path: '/map-location-picker-page',
guards: [
authGuard,
duplicateGuard,
],
),
];
}
@ -1642,40 +1618,6 @@ class ActivitiesRouteArgs {
}
}
/// generated route for
/// [MapLocationPickerPage]
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
MapLocationPickerRoute({
Key? key,
LatLng? initialLatLng,
}) : super(
MapLocationPickerRoute.name,
path: '/map-location-picker-page',
args: MapLocationPickerRouteArgs(
key: key,
initialLatLng: initialLatLng,
),
);
static const String name = 'MapLocationPickerRoute';
}
class MapLocationPickerRouteArgs {
const MapLocationPickerRouteArgs({
this.key,
this.initialLatLng,
});
final Key? key;
final LatLng? initialLatLng;
@override
String toString() {
return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}';
}
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {

View file

@ -256,8 +256,6 @@ class Asset {
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed ||
a.exifInfo?.latitude != exifInfo?.latitude ||
a.exifInfo?.longitude != exifInfo?.longitude ||
// no local stack count or different count from remote
((stackCount == null && a.stackCount != null) ||
(stackCount != null &&

View file

@ -6,7 +6,7 @@ part of 'api.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$apiServiceHash() => r'5b8beddb448316bdae5e3963ff77601653715729';
String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d';
/// See also [apiService].
@ProviderFor(apiService)

View file

@ -1,13 +1,10 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/server_info/server_version.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/debounce.dart';
@ -17,33 +14,13 @@ import 'package:socket_io_client/socket_io_client.dart';
enum PendingAction {
assetDelete,
assetUploaded,
assetHidden,
}
class PendingChange {
final String id;
final PendingAction action;
final dynamic value;
const PendingChange(
this.id,
this.action,
this.value,
);
@override
String toString() => 'PendingChange(id: $id, action: $action, value: $value)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PendingChange && other.id == id && other.action == action;
}
@override
int get hashCode => id.hashCode ^ action.hashCode;
const PendingChange(this.action, this.value);
}
class WebsocketState {
@ -154,7 +131,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_asset_trash', _handleServerUpdates);
socket.on('on_asset_restore', _handleServerUpdates);
socket.on('on_asset_update', _handleServerUpdates);
socket.on('on_asset_hidden', _handleOnAssetHidden);
socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
@ -187,77 +163,34 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
void addPendingChange(PendingAction action, dynamic value) {
final now = DateTime.now();
state = state.copyWith(
pendingChanges: [
...state.pendingChanges,
PendingChange(now.millisecondsSinceEpoch.toString(), action, value),
],
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
);
_debounce(handlePendingChanges);
}
Future<void> _handlePendingDeletes() async {
void handlePendingChanges() {
final deleteChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetDelete)
.toList();
if (deleteChanges.isNotEmpty) {
List<String> remoteIds =
deleteChanges.map((a) => a.value.toString()).toList();
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
_ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
state = state.copyWith(
pendingChanges: state.pendingChanges
.whereNot((c) => deleteChanges.contains(c))
.where((c) => c.action != PendingAction.assetDelete)
.toList(),
);
}
}
Future<void> _handlePendingUploaded() async {
final uploadedChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetUploaded)
.toList();
if (uploadedChanges.isNotEmpty) {
List<AssetResponseDto?> remoteAssets = uploadedChanges
.map((a) => AssetResponseDto.fromJson(a.value))
.toList();
for (final dto in remoteAssets) {
void _handleOnUploadSuccess(dynamic data) {
final dto = AssetResponseDto.fromJson(data);
if (dto != null) {
final newAsset = Asset.remote(dto);
await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
_ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
}
}
state = state.copyWith(
pendingChanges: state.pendingChanges
.whereNot((c) => uploadedChanges.contains(c))
.toList(),
);
}
}
Future<void> _handlingPendingHidden() async {
final hiddenChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetHidden)
.toList();
if (hiddenChanges.isNotEmpty) {
List<String> remoteIds =
hiddenChanges.map((a) => a.value.toString()).toList();
final db = _ref.watch(dbProvider);
await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds));
state = state.copyWith(
pendingChanges: state.pendingChanges
.whereNot((c) => hiddenChanges.contains(c))
.toList(),
);
}
}
void handlePendingChanges() async {
await _handlePendingUploaded();
await _handlePendingDeletes();
await _handlingPendingHidden();
}
void _handleOnConfigUpdate(dynamic _) {
_ref.read(serverInfoProvider.notifier).getServerFeatures();
@ -269,14 +202,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_ref.read(assetProvider.notifier).getAllAsset();
}
void _handleOnUploadSuccess(dynamic data) =>
addPendingChange(PendingAction.assetUploaded, data);
void _handleOnAssetDelete(dynamic data) =>
void _handleOnAssetDelete(dynamic data) {
addPendingChange(PendingAction.assetDelete, data);
void _handleOnAssetHidden(dynamic data) =>
addPendingChange(PendingAction.assetHidden, data);
_debounce(handlePendingChanges);
}
_handleReleaseUpdates(dynamic data) {
// Json guard

View file

@ -11,7 +11,6 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:isar/isar.dart';
import 'package:latlong2/latlong.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@ -49,23 +48,28 @@ class AssetService {
return changes;
}
/// Returns `(null, null)` if changes are invalid -> requires full sync
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
/// Returns `(null, null, time)` if changes are invalid -> requires full sync
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
_getRemoteAssetChanges(User user, DateTime since) async {
final deleted = await _apiService.auditApi
.getAuditDeletes(EntityType.ASSET, since, userId: user.id);
if (deleted == null || deleted.needsFullSync) return (null, null);
if (deleted == null || deleted.needsFullSync) {
return (null, null, deleted?.requestedAt);
}
final assetDto = await _apiService.assetApi
.getAllAssets(userId: user.id, updatedAfter: since);
if (assetDto == null) return (null, null);
return (assetDto.map(Asset.remote).toList(), deleted.ids);
if (assetDto == null) return (null, null, deleted.requestedAt);
return (
assetDto.map(Asset.remote).toList(),
deleted.ids,
deleted.requestedAt
);
}
/// Returns `null` if the server state did not change, else list of assets
Future<List<Asset>?> _getRemoteAssets(User user) async {
Future<List<Asset>?> _getRemoteAssets(User user, DateTime now) async {
const int chunkSize = 5000;
try {
final DateTime now = DateTime.now().toUtc();
final List<Asset> allAssets = [];
for (int i = 0;; i += chunkSize) {
final List<AssetResponseDto>? assets =
@ -182,27 +186,4 @@ class AssetService {
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
}
Future<List<Asset?>> changeDateTime(
List<Asset> assets,
String updatedDt,
) {
return updateAssets(
assets,
UpdateAssetDto(dateTimeOriginal: updatedDt),
);
}
Future<List<Asset?>> changeLocation(
List<Asset> assets,
LatLng location,
) {
return updateAssets(
assets,
UpdateAssetDto(
latitude: location.latitude,
longitude: location.longitude,
),
);
}
}

View file

@ -41,17 +41,20 @@ class SyncService {
/// Returns `true` if there were any changes
Future<bool> syncRemoteAssetsToDb(
User user,
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
Function(
User user,
DateTime since,
) getChangedAssets,
FutureOr<List<Asset>?> Function(User user) loadAssets,
FutureOr<List<Asset>?> Function(User user, DateTime now) loadAssets,
) =>
_lock.run(
() async =>
await _syncRemoteAssetChanges(user, getChangedAssets) ??
await _syncRemoteAssetsFull(user, loadAssets),
);
_lock.run(() async {
final (changes, serverTime) =
await _syncRemoteAssetChanges(user, getChangedAssets);
if (changes != null) return changes;
final time = serverTime ?? DateTime.now().toUtc();
return await _syncRemoteAssetsFull(user, time, loadAssets);
});
/// Syncs remote albums to the database
/// returns `true` if there were any changes
@ -146,19 +149,22 @@ class SyncService {
return true;
}
/// Efficiently syncs assets via changes. Returns `null` when a full sync is required.
Future<bool?> _syncRemoteAssetChanges(
/// Efficiently syncs assets via changes. Returns `(null, serverTime)` when a full sync is required.
Future<(bool?, DateTime? serverTime)> _syncRemoteAssetChanges(
User user,
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
Future<(List<Asset>? toUpsert, List<String>? toDelete, DateTime? time)>
Function(
User user,
DateTime since,
) getChangedAssets,
) async {
final DateTime? since = _db.eTags.getByIdSync(user.id)?.time?.toUtc();
if (since == null) return null;
final DateTime now = DateTime.now();
final (toUpsert, toDelete) = await getChangedAssets(user, since);
if (toUpsert == null || toDelete == null) return null;
final DateTime now = DateTime.now().toUtc();
final (toUpsert, toDelete, serverTime) =
await getChangedAssets(user, since ?? now);
if (since == null || toUpsert == null || toDelete == null) {
return (null, serverTime);
}
try {
if (toDelete.isNotEmpty) {
await handleRemoteAssetRemoval(toDelete);
@ -168,14 +174,14 @@ class SyncService {
await upsertAssetsWithExif(updated);
}
if (toUpsert.isNotEmpty || toDelete.isNotEmpty) {
await _updateUserAssetsETag(user, now);
return true;
await _updateUserAssetsETag(user, serverTime ?? now);
return (true, serverTime);
}
return false;
return (false, serverTime);
} on IsarError catch (e) {
_log.severe("Failed to sync remote assets to db: $e");
}
return null;
return (null, serverTime);
}
/// Deletes remote-only assets, updates merged assets to be local-only
@ -202,11 +208,11 @@ class SyncService {
/// Syncs assets by loading and comparing all assets from the server.
Future<bool> _syncRemoteAssetsFull(
User user,
FutureOr<List<Asset>?> Function(User user) loadAssets,
final User user,
final DateTime now,
final FutureOr<List<Asset>?> Function(User user, DateTime now) loadAssets,
) async {
final DateTime now = DateTime.now().toUtc();
final List<Asset>? remote = await loadAssets(user);
final List<Asset>? remote = await loadAssets(user, now);
if (remote == null) {
return false;
}
@ -401,10 +407,6 @@ class SyncService {
final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a));
} else {
_log.warning(
"Failed to add album from server: assetCount ${dto.assetCount} != "
"asset array length ${dto.assets.length} for album ${dto.albumName}");
}
}

View file

@ -1,260 +0,0 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/timezone.dart';
Future<String?> showDateTimePicker({
required BuildContext context,
DateTime? initialDateTime,
String? initialTZ,
Duration? initialTZOffset,
}) {
return showDialog<String?>(
context: context,
builder: (context) => _DateTimePicker(
initialDateTime: initialDateTime,
initialTZ: initialTZ,
initialTZOffset: initialTZOffset,
),
);
}
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
}
class _DateTimePicker extends HookWidget {
final DateTime? initialDateTime;
final String? initialTZ;
final Duration? initialTZOffset;
const _DateTimePicker({
this.initialDateTime,
this.initialTZ,
this.initialTZOffset,
});
_TimeZoneOffset _getInitiationLocation() {
if (initialTZ != null) {
try {
return _TimeZoneOffset.fromLocation(
tz.timeZoneDatabase.get(initialTZ!),
);
} on LocationNotFoundException {
// no-op
}
}
Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset;
if (tzOffset != null) {
final offsetInMilli = tzOffset.inMilliseconds;
// get all locations with matching offset
final locations = tz.timeZoneDatabase.locations.values.where(
(location) => location.currentTimeZone.offset == offsetInMilli,
);
// Prefer locations with abbreviation first
final location = locations.firstWhereOrNull(
(e) => !e.currentTimeZone.abbreviation.contains("0"),
) ??
locations.firstOrNull;
if (location != null) {
return _TimeZoneOffset.fromLocation(location);
}
}
return _TimeZoneOffset.fromLocation(tz.getLocation("UTC"));
}
// returns a list of location<name> along with it's offset in duration
List<_TimeZoneOffset> getAllTimeZones() {
return tz.timeZoneDatabase.locations.values
.where((l) => !l.currentTimeZone.abbreviation.contains("0"))
.map(_TimeZoneOffset.fromLocation)
.sorted()
.toList();
}
@override
Widget build(BuildContext context) {
final date = useState<DateTime>(initialDateTime ?? DateTime.now());
final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation());
final timeZones = useMemoized(() => getAllTimeZones(), const []);
void pickDate() async {
final now = DateTime.now();
// Handles cases where the date from the asset is far off in the future
final initialDate = date.value.isAfter(now) ? now : date.value;
final newDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(1800),
lastDate: now,
);
if (newDate == null) {
return;
}
final newTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(date.value),
);
if (newTime == null) {
return;
}
date.value = newDate.copyWith(hour: newTime.hour, minute: newTime.minute);
}
void popWithDateTime() {
final formattedDateTime =
DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value);
final dtWithOffset = formattedDateTime +
Duration(milliseconds: tzOffset.value.offsetInMilliseconds)
.formatAsOffset();
context.pop(dtWithOffset);
}
return AlertDialog(
contentPadding: const EdgeInsets.all(30),
alignment: Alignment.center,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_date_time_dialog_date_time",
textAlign: TextAlign.center,
).tr(),
TextButton.icon(
onPressed: pickDate,
icon: Text(
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
style: context.textTheme.bodyLarge
?.copyWith(color: context.primaryColor),
),
label: const Icon(
Icons.edit_outlined,
size: 18,
),
),
const Text(
"edit_date_time_dialog_timezone",
textAlign: TextAlign.center,
).tr(),
DropdownMenu(
menuHeight: 300,
width: 280,
inputDecorationTheme: const InputDecorationTheme(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
trailingIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: Icon(
Icons.arrow_drop_down,
color: context.primaryColor,
),
),
textStyle: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
menuStyle: const MenuStyle(
fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
alignment: Alignment(-1.25, 0.5),
),
onSelected: (value) => tzOffset.value = value!,
initialSelection: tzOffset.value,
dropdownMenuEntries: timeZones
.map(
(t) => DropdownMenuEntry<_TimeZoneOffset>(
value: t,
label: t.display,
style: ButtonStyle(
textStyle: MaterialStatePropertyAll(
context.textTheme.bodyMedium,
),
),
),
)
.toList(),
),
],
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(
"action_common_cancel",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colorScheme.error,
),
).tr(),
),
TextButton(
onPressed: popWithDateTime,
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
),
],
);
}
}
class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
final String display;
final Location location;
const _TimeZoneOffset({
required this.display,
required this.location,
});
_TimeZoneOffset copyWith({
String? display,
Location? location,
}) {
return _TimeZoneOffset(
display: display ?? this.display,
location: location ?? this.location,
);
}
int get offsetInMilliseconds => location.currentTimeZone.offset;
_TimeZoneOffset.fromLocation(tz.Location l)
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
location = l;
@override
int compareTo(_TimeZoneOffset other) {
return offsetInMilliseconds.compareTo(other.offsetInMilliseconds);
}
@override
String toString() =>
'_TimeZoneOffset(display: $display, location: $location)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is _TimeZoneOffset &&
other.display == display &&
other.offsetInMilliseconds == offsetInMilliseconds;
}
@override
int get hashCode =>
display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode;
}

View file

@ -1,256 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:latlong2/latlong.dart';
Future<LatLng?> showLocationPicker({
required BuildContext context,
LatLng? initialLatLng,
}) {
return showDialog<LatLng?>(
context: context,
useRootNavigator: false,
builder: (ctx) => _LocationPicker(
initialLatLng: initialLatLng,
),
);
}
enum _LocationPickerMode { map, manual }
bool _validateLat(String value) {
final l = double.tryParse(value);
return l != null && l > -90 && l < 90;
}
bool _validateLong(String value) {
final l = double.tryParse(value);
return l != null && l > -180 && l < 180;
}
class _LocationPicker extends HookWidget {
final LatLng? initialLatLng;
const _LocationPicker({
this.initialLatLng,
});
@override
Widget build(BuildContext context) {
final latitude = useState(initialLatLng?.latitude ?? 0.0);
final longitude = useState(initialLatLng?.longitude ?? 0.0);
final latlng = LatLng(latitude.value, longitude.value);
final pickerMode = useState(_LocationPickerMode.map);
final latitudeController = useTextEditingController();
final isValidLatitude = useState(true);
final latitiudeFocusNode = useFocusNode();
final longitudeController = useTextEditingController();
final longitudeFocusNode = useFocusNode();
final isValidLongitude = useState(true);
void validateInputs() {
isValidLatitude.value = _validateLat(latitudeController.text);
if (isValidLatitude.value) {
latitude.value = latitudeController.text.toDouble();
}
isValidLongitude.value = _validateLong(longitudeController.text);
if (isValidLongitude.value) {
longitude.value = longitudeController.text.toDouble();
}
}
void validateAndPop() {
if (pickerMode.value == _LocationPickerMode.manual) {
validateInputs();
}
if (isValidLatitude.value && isValidLongitude.value) {
return context.pop(latlng);
}
}
List<Widget> buildMapPickerMode() {
return [
TextButton.icon(
icon: Text(
"${latitude.value.toStringAsFixed(4)}, ${longitude.value.toStringAsFixed(4)}",
),
label: const Icon(Icons.edit_outlined, size: 16),
onPressed: () {
latitudeController.text = latitude.value.toStringAsFixed(4);
longitudeController.text = longitude.value.toStringAsFixed(4);
pickerMode.value = _LocationPickerMode.manual;
},
),
const SizedBox(
height: 12,
),
MapThumbnail(
coords: latlng,
height: 200,
width: 200,
zoom: 6,
showAttribution: false,
onTap: (p0, p1) async {
final newLatLng = await context.autoPush<LatLng?>(
MapLocationPickerRoute(initialLatLng: latlng),
);
if (newLatLng != null) {
latitude.value = newLatLng.latitude;
longitude.value = newLatLng.longitude;
}
},
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(
latitude.value,
longitude.value,
),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png'),
),
),
],
),
];
}
List<Widget> buildManualPickerMode() {
return [
TextButton.icon(
icon: const Text("location_picker_choose_on_map").tr(),
label: const Icon(Icons.map_outlined, size: 16),
onPressed: () {
validateInputs();
if (isValidLatitude.value && isValidLongitude.value) {
pickerMode.value = _LocationPickerMode.map;
}
},
),
const SizedBox(
height: 12,
),
TextField(
controller: latitudeController,
focusNode: latitiudeFocusNode,
textInputAction: TextInputAction.done,
autofocus: false,
decoration: InputDecoration(
labelText: 'location_picker_latitude'.tr(),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
floatingLabelBehavior: FloatingLabelBehavior.auto,
border: const OutlineInputBorder(),
hintText: 'location_picker_latitude_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
errorText: isValidLatitude.value
? null
: "location_picker_latitude_error".tr(),
),
onEditingComplete: () {
isValidLatitude.value = _validateLat(latitudeController.text);
if (isValidLatitude.value) {
latitude.value = latitudeController.text.toDouble();
longitudeFocusNode.requestFocus();
}
},
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [LengthLimitingTextInputFormatter(8)],
onTapOutside: (_) => latitiudeFocusNode.unfocus(),
),
const SizedBox(
height: 24,
),
TextField(
controller: longitudeController,
focusNode: longitudeFocusNode,
textInputAction: TextInputAction.done,
autofocus: false,
decoration: InputDecoration(
labelText: 'location_picker_longitude'.tr(),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
floatingLabelBehavior: FloatingLabelBehavior.auto,
border: const OutlineInputBorder(),
hintText: 'location_picker_longitude_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
errorText: isValidLongitude.value
? null
: "location_picker_longitude_error".tr(),
),
onEditingComplete: () {
isValidLongitude.value = _validateLong(longitudeController.text);
if (isValidLongitude.value) {
longitude.value = longitudeController.text.toDouble();
longitudeFocusNode.unfocus();
}
},
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [LengthLimitingTextInputFormatter(8)],
onTapOutside: (_) => longitudeFocusNode.unfocus(),
),
];
}
return AlertDialog(
contentPadding: const EdgeInsets.all(30),
alignment: Alignment.center,
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_location_dialog_title",
textAlign: TextAlign.center,
).tr(),
const SizedBox(
height: 12,
),
if (pickerMode.value == _LocationPickerMode.manual)
...buildManualPickerMode(),
if (pickerMode.value == _LocationPickerMode.map)
...buildMapPickerMode(),
],
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(
"action_common_cancel",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colorScheme.error,
),
).tr(),
),
TextButton(
onPressed: validateAndPop,
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
),
],
);
}
}

View file

@ -15,7 +15,7 @@ class ScaffoldErrorBody extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"scaffold_body_error_occurred",
"scaffold_body_error_occured",
style: context.textTheme.displayMedium,
textAlign: TextAlign.center,
).tr(),

View file

@ -20,7 +20,7 @@ final immichThemeProvider = StateProvider<ThemeMode>((ref) {
}
});
final ThemeData base = ThemeData(
ThemeData base = ThemeData(
chipTheme: const ChipThemeData(
side: BorderSide.none,
),
@ -30,7 +30,7 @@ final ThemeData base = ThemeData(
),
);
final ThemeData immichLightTheme = ThemeData(
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
@ -153,7 +153,7 @@ final ThemeData immichLightTheme = ThemeData(
),
);
final ThemeData immichDarkTheme = ThemeData(
ThemeData immichDarkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
primarySwatch: Colors.indigo,

View file

@ -2,17 +2,12 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/date_time_picker.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/location_picker.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:latlong2/latlong.dart';
void handleShareAssets(
WidgetRef ref,
@ -90,60 +85,3 @@ Future<void> handleFavoriteAssets(
}
}
}
Future<void> handleEditDateTime(
WidgetRef ref,
BuildContext context,
List<Asset> selection,
) async {
DateTime? initialDate;
String? timeZone;
Duration? offset;
if (selection.length == 1) {
final asset = selection.first;
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset();
initialDate = dt;
offset = oft;
timeZone = assetWithExif.exifInfo?.timeZone;
}
final dateTime = await showDateTimePicker(
context: context,
initialDateTime: initialDate,
initialTZ: timeZone,
initialTZOffset: offset,
);
if (dateTime == null) {
return;
}
ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime);
}
Future<void> handleEditLocation(
WidgetRef ref,
BuildContext context,
List<Asset> selection,
) async {
LatLng? initialLatLng;
if (selection.length == 1) {
final asset = selection.first;
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
if (assetWithExif.exifInfo?.latitude != null &&
assetWithExif.exifInfo?.longitude != null) {
initialLatLng = LatLng(
assetWithExif.exifInfo!.latitude!,
assetWithExif.exifInfo!.longitude!,
);
}
}
final location = await showLocationPicker(
context: context,
initialLatLng: initialLatLng,
);
if (location == null) {
return;
}
ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
}

View file

@ -24,10 +24,6 @@ doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md
doc/AssetFaceResponseDto.md
doc/AssetFaceUpdateDto.md
doc/AssetFaceUpdateItem.md
doc/AssetFaceWithoutPersonResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetIdsDto.md
doc/AssetIdsResponseDto.md
@ -64,8 +60,6 @@ doc/DownloadInfoDto.md
doc/DownloadResponseDto.md
doc/EntityType.md
doc/ExifResponseDto.md
doc/FaceApi.md
doc/FaceDto.md
doc/FileChecksumDto.md
doc/FileChecksumResponseDto.md
doc/FileReportDto.md
@ -106,7 +100,6 @@ doc/PersonApi.md
doc/PersonResponseDto.md
doc/PersonStatisticsResponseDto.md
doc/PersonUpdateDto.md
doc/PersonWithFacesResponseDto.md
doc/QueueStatusDto.md
doc/ReactionLevel.md
doc/ReactionType.md
@ -184,7 +177,6 @@ lib/api/api_key_api.dart
lib/api/asset_api.dart
lib/api/audit_api.dart
lib/api/authentication_api.dart
lib/api/face_api.dart
lib/api/job_api.dart
lib/api/library_api.dart
lib/api/o_auth_api.dart
@ -221,10 +213,6 @@ lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart
lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.dart
lib/model/asset_face_response_dto.dart
lib/model/asset_face_update_dto.dart
lib/model/asset_face_update_item.dart
lib/model/asset_face_without_person_response_dto.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_ids_dto.dart
lib/model/asset_ids_response_dto.dart
@ -259,7 +247,6 @@ lib/model/download_info_dto.dart
lib/model/download_response_dto.dart
lib/model/entity_type.dart
lib/model/exif_response_dto.dart
lib/model/face_dto.dart
lib/model/file_checksum_dto.dart
lib/model/file_checksum_response_dto.dart
lib/model/file_report_dto.dart
@ -295,7 +282,6 @@ lib/model/people_update_item.dart
lib/model/person_response_dto.dart
lib/model/person_statistics_response_dto.dart
lib/model/person_update_dto.dart
lib/model/person_with_faces_response_dto.dart
lib/model/queue_status_dto.dart
lib/model/reaction_level.dart
lib/model/reaction_type.dart
@ -381,10 +367,6 @@ test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart
test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_test.dart
test/asset_face_response_dto_test.dart
test/asset_face_update_dto_test.dart
test/asset_face_update_item_test.dart
test/asset_face_without_person_response_dto_test.dart
test/asset_file_upload_response_dto_test.dart
test/asset_ids_dto_test.dart
test/asset_ids_response_dto_test.dart
@ -421,8 +403,6 @@ test/download_info_dto_test.dart
test/download_response_dto_test.dart
test/entity_type_test.dart
test/exif_response_dto_test.dart
test/face_api_test.dart
test/face_dto_test.dart
test/file_checksum_dto_test.dart
test/file_checksum_response_dto_test.dart
test/file_report_dto_test.dart
@ -463,7 +443,6 @@ test/person_api_test.dart
test/person_response_dto_test.dart
test/person_statistics_response_dto_test.dart
test/person_update_dto_test.dart
test/person_with_faces_response_dto_test.dart
test/queue_status_dto_test.dart
test/reaction_level_test.dart
test/reaction_type_test.dart

View file

@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.89.0
- API version: 1.88.2
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@ -133,8 +133,6 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face |
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
@ -155,14 +153,12 @@ Class | Method | HTTP request | Description
*PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner |
*PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
*PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /partner/{id} |
*PersonApi* | [**createPerson**](doc//PersonApi.md#createperson) | **POST** /person |
*PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person |
*PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} |
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
*PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
@ -228,10 +224,6 @@ Class | Method | HTTP request | Description
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
- [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md)
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetIdsDto](doc//AssetIdsDto.md)
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
@ -266,7 +258,6 @@ Class | Method | HTTP request | Description
- [DownloadResponseDto](doc//DownloadResponseDto.md)
- [EntityType](doc//EntityType.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [FaceDto](doc//FaceDto.md)
- [FileChecksumDto](doc//FileChecksumDto.md)
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
- [FileReportDto](doc//FileReportDto.md)
@ -302,7 +293,6 @@ Class | Method | HTTP request | Description
- [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md)
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)

View file

@ -8,12 +8,9 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**ids** | **List<String>** | | [default to const []]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**latitude** | **num** | | [optional]
**longitude** | **num** | | [optional]
**removeParent** | **bool** | | [optional]
**stackParentId** | **String** | | [optional]

View file

@ -1,22 +0,0 @@
# openapi.model.AssetFaceResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**boundingBoxX1** | **int** | |
**boundingBoxX2** | **int** | |
**boundingBoxY1** | **int** | |
**boundingBoxY2** | **int** | |
**id** | **String** | |
**imageHeight** | **int** | |
**imageWidth** | **int** | |
**person** | [**PersonResponseDto**](PersonResponseDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,15 +0,0 @@
# openapi.model.AssetFaceUpdateDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**data** | [**List<AssetFaceUpdateItem>**](AssetFaceUpdateItem.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,16 +0,0 @@
# openapi.model.AssetFaceUpdateItem
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetId** | **String** | |
**personId** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,21 +0,0 @@
# openapi.model.AssetFaceWithoutPersonResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**boundingBoxX1** | **int** | |
**boundingBoxX2** | **int** | |
**boundingBoxY1** | **int** | |
**boundingBoxY2** | **int** | |
**id** | **String** | |
**imageHeight** | **int** | |
**imageWidth** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -30,7 +30,7 @@ Name | Type | Description | Notes
**originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
**ownerId** | **String** | |
**people** | [**List<PersonWithFacesResponseDto>**](PersonWithFacesResponseDto.md) | | [optional] [default to const []]
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]

View file

@ -10,6 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**ids** | **List<String>** | | [default to const []]
**needsFullSync** | **bool** | |
**requestedAt** | [**DateTime**](DateTime.md) | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,127 +0,0 @@
# openapi.api.FaceApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getFaces**](FaceApi.md#getfaces) | **GET** /face |
[**reassignFacesById**](FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
# **getFaces**
> List<AssetFaceResponseDto> getFaces(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = FaceApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getFaces(id);
print(result);
} catch (e) {
print('Exception when calling FaceApi->getFaces: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
[**List<AssetFaceResponseDto>**](AssetFaceResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **reassignFacesById**
> PersonResponseDto reassignFacesById(id, faceDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = FaceApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final faceDto = FaceDto(); // FaceDto |
try {
final result = api_instance.reassignFacesById(id, faceDto);
print(result);
} catch (e) {
print('Exception when calling FaceApi->reassignFacesById: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**faceDto** | [**FaceDto**](FaceDto.md)| |
### Return type
[**PersonResponseDto**](PersonResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View file

@ -1,15 +0,0 @@
# openapi.model.FaceDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -9,69 +9,16 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**createPerson**](PersonApi.md#createperson) | **POST** /person |
[**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person |
[**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} |
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
[**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
[**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
# **createPerson**
> PersonResponseDto createPerson()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = PersonApi();
try {
final result = api_instance.createPerson();
print(result);
} catch (e) {
print('Exception when calling PersonApi->createPerson: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**PersonResponseDto**](PersonResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAllPeople**
> PeopleResponseDto getAllPeople(withHidden)
@ -404,63 +351,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)
# **reassignFaces**
> List<PersonResponseDto> reassignFaces(id, assetFaceUpdateDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = PersonApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto |
try {
final result = api_instance.reassignFaces(id, assetFaceUpdateDto);
print(result);
} catch (e) {
print('Exception when calling PersonApi->reassignFaces: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| |
### Return type
[**List<PersonResponseDto>**](PersonResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updatePeople**
> List<BulkIdResponseDto> updatePeople(peopleUpdateDto)

View file

@ -1,20 +0,0 @@
# openapi.model.PersonWithFacesResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**birthDate** | [**DateTime**](DateTime.md) | |
**faces** | [**List<AssetFaceWithoutPersonResponseDto>**](AssetFaceWithoutPersonResponseDto.md) | | [default to const []]
**id** | **String** | |
**isHidden** | **bool** | |
**name** | **String** | |
**thumbnailPath** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -8,12 +8,9 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**description** | **String** | | [optional]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**latitude** | **num** | | [optional]
**longitude** | **num** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -34,7 +34,6 @@ part 'api/album_api.dart';
part 'api/asset_api.dart';
part 'api/audit_api.dart';
part 'api/authentication_api.dart';
part 'api/face_api.dart';
part 'api/job_api.dart';
part 'api/library_api.dart';
part 'api/o_auth_api.dart';
@ -64,10 +63,6 @@ part 'model/asset_bulk_upload_check_dto.dart';
part 'model/asset_bulk_upload_check_item.dart';
part 'model/asset_bulk_upload_check_response_dto.dart';
part 'model/asset_bulk_upload_check_result.dart';
part 'model/asset_face_response_dto.dart';
part 'model/asset_face_update_dto.dart';
part 'model/asset_face_update_item.dart';
part 'model/asset_face_without_person_response_dto.dart';
part 'model/asset_file_upload_response_dto.dart';
part 'model/asset_ids_dto.dart';
part 'model/asset_ids_response_dto.dart';
@ -102,7 +97,6 @@ part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart';
part 'model/entity_type.dart';
part 'model/exif_response_dto.dart';
part 'model/face_dto.dart';
part 'model/file_checksum_dto.dart';
part 'model/file_checksum_response_dto.dart';
part 'model/file_report_dto.dart';
@ -138,7 +132,6 @@ part 'model/people_update_item.dart';
part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart';
part 'model/person_update_dto.dart';
part 'model/person_with_faces_response_dto.dart';
part 'model/queue_status_dto.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';

View file

@ -1,122 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class FaceApi {
FaceApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'GET /face' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getFacesWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/face';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'id', id));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<List<AssetFaceResponseDto>?> getFaces(String id,) async {
final response = await getFacesWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetFaceResponseDto>') as List)
.cast<AssetFaceResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PUT /face/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [FaceDto] faceDto (required):
Future<Response> reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async {
// ignore: prefer_const_declarations
final path = r'/face/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = faceDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [FaceDto] faceDto (required):
Future<PersonResponseDto?> reassignFacesById(String id, FaceDto faceDto,) async {
final response = await reassignFacesByIdWithHttpInfo(id, faceDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
}
return null;
}
}

View file

@ -16,47 +16,6 @@ class PersonApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /person' operation and returns the [Response].
Future<Response> createPersonWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/person';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<PersonResponseDto?> createPerson() async {
final response = await createPersonWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /person' operation and returns the [Response].
/// Parameters:
///
@ -358,61 +317,6 @@ class PersonApi {
return null;
}
/// Performs an HTTP 'PUT /person/{id}/reassign' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<Response> reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/person/{id}/reassign'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = assetFaceUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<List<PersonResponseDto>?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
.cast<PersonResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
/// Parameters:
///

View file

@ -215,14 +215,6 @@ class ApiClient {
return AssetBulkUploadCheckResponseDto.fromJson(value);
case 'AssetBulkUploadCheckResult':
return AssetBulkUploadCheckResult.fromJson(value);
case 'AssetFaceResponseDto':
return AssetFaceResponseDto.fromJson(value);
case 'AssetFaceUpdateDto':
return AssetFaceUpdateDto.fromJson(value);
case 'AssetFaceUpdateItem':
return AssetFaceUpdateItem.fromJson(value);
case 'AssetFaceWithoutPersonResponseDto':
return AssetFaceWithoutPersonResponseDto.fromJson(value);
case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value);
case 'AssetIdsDto':
@ -291,8 +283,6 @@ class ApiClient {
return EntityTypeTypeTransformer().decode(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'FaceDto':
return FaceDto.fromJson(value);
case 'FileChecksumDto':
return FileChecksumDto.fromJson(value);
case 'FileChecksumResponseDto':
@ -363,8 +353,6 @@ class ApiClient {
return PersonStatisticsResponseDto.fromJson(value);
case 'PersonUpdateDto':
return PersonUpdateDto.fromJson(value);
case 'PersonWithFacesResponseDto':
return PersonWithFacesResponseDto.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'ReactionLevel':

View file

@ -13,24 +13,13 @@ part of openapi.api;
class AssetBulkUpdateDto {
/// Returns a new [AssetBulkUpdateDto] instance.
AssetBulkUpdateDto({
this.dateTimeOriginal,
this.ids = const [],
this.isArchived,
this.isFavorite,
this.latitude,
this.longitude,
this.removeParent,
this.stackParentId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? dateTimeOriginal;
List<String> ids;
///
@ -49,22 +38,6 @@ class AssetBulkUpdateDto {
///
bool? isFavorite;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@ -83,37 +56,26 @@ class AssetBulkUpdateDto {
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.ids == ids &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
other.removeParent == removeParent &&
other.stackParentId == stackParentId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(removeParent == null ? 0 : removeParent!.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode);
@override
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
} else {
// json[r'dateTimeOriginal'] = null;
}
json[r'ids'] = this.ids;
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
@ -125,16 +87,6 @@ class AssetBulkUpdateDto {
} else {
// json[r'isFavorite'] = null;
}
if (this.latitude != null) {
json[r'latitude'] = this.latitude;
} else {
// json[r'latitude'] = null;
}
if (this.longitude != null) {
json[r'longitude'] = this.longitude;
} else {
// json[r'longitude'] = null;
}
if (this.removeParent != null) {
json[r'removeParent'] = this.removeParent;
} else {
@ -156,18 +108,11 @@ class AssetBulkUpdateDto {
final json = value.cast<String, dynamic>();
return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
ids: json[r'ids'] is List
? (json[r'ids'] as List).cast<String>()
: const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: json[r'latitude'] == null
? null
: num.parse(json[r'latitude'].toString()),
longitude: json[r'longitude'] == null
? null
: num.parse(json[r'longitude'].toString()),
removeParent: mapValueOfType<bool>(json, r'removeParent'),
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
);

View file

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

View file

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

View file

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

View file

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

View file

@ -104,7 +104,7 @@ class AssetResponseDto {
String ownerId;
List<PersonWithFacesResponseDto> people;
List<PersonResponseDto> people;
bool resized;
@ -299,7 +299,7 @@ class AssetResponseDto {
originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetResponseDto.listFromJson(json[r'stack']),

View file

@ -15,30 +15,46 @@ class AuditDeletesResponseDto {
AuditDeletesResponseDto({
this.ids = const [],
required this.needsFullSync,
this.requestedAt,
});
List<String> ids;
bool needsFullSync;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? requestedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is AuditDeletesResponseDto &&
other.ids == ids &&
other.needsFullSync == needsFullSync;
other.needsFullSync == needsFullSync &&
other.requestedAt == requestedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(ids.hashCode) +
(needsFullSync.hashCode);
(needsFullSync.hashCode) +
(requestedAt == null ? 0 : requestedAt!.hashCode);
@override
String toString() => 'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync]';
String toString() => 'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync, requestedAt=$requestedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'ids'] = this.ids;
json[r'needsFullSync'] = this.needsFullSync;
if (this.requestedAt != null) {
json[r'requestedAt'] = this.requestedAt!.toUtc().toIso8601String();
} else {
// json[r'requestedAt'] = null;
}
return json;
}
@ -54,6 +70,7 @@ class AuditDeletesResponseDto {
? (json[r'ids'] as List).cast<String>()
: const [],
needsFullSync: mapValueOfType<bool>(json, r'needsFullSync')!,
requestedAt: mapDateTime(json, r'requestedAt', ''),
);
}
return null;

View file

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

View file

@ -27,7 +27,6 @@ class JobCommand {
static const pause = JobCommand._(r'pause');
static const resume = JobCommand._(r'resume');
static const empty = JobCommand._(r'empty');
static const clearFailed = JobCommand._(r'clear-failed');
/// List of all possible values in this [enum][JobCommand].
static const values = <JobCommand>[
@ -35,7 +34,6 @@ class JobCommand {
pause,
resume,
empty,
clearFailed,
];
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
@ -78,7 +76,6 @@ class JobCommandTypeTransformer {
case r'pause': return JobCommand.pause;
case r'resume': return JobCommand.resume;
case r'empty': return JobCommand.empty;
case r'clear-failed': return JobCommand.clearFailed;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View file

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

View file

@ -13,22 +13,11 @@ part of openapi.api;
class UpdateAssetDto {
/// Returns a new [UpdateAssetDto] instance.
UpdateAssetDto({
this.dateTimeOriginal,
this.description,
this.isArchived,
this.isFavorite,
this.latitude,
this.longitude,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? dateTimeOriginal;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@ -53,51 +42,24 @@ class UpdateAssetDto {
///
bool? isFavorite;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.description == description &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude;
other.isFavorite == isFavorite;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode);
(isFavorite == null ? 0 : isFavorite!.hashCode);
@override
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
} else {
// json[r'dateTimeOriginal'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
@ -113,16 +75,6 @@ class UpdateAssetDto {
} else {
// json[r'isFavorite'] = null;
}
if (this.latitude != null) {
json[r'latitude'] = this.latitude;
} else {
// json[r'latitude'] = null;
}
if (this.longitude != null) {
json[r'longitude'] = this.longitude;
} else {
// json[r'longitude'] = null;
}
return json;
}
@ -134,16 +86,9 @@ class UpdateAssetDto {
final json = value.cast<String, dynamic>();
return UpdateAssetDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
description: mapValueOfType<String>(json, r'description'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: json[r'latitude'] == null
? null
: num.parse(json[r'latitude'].toString()),
longitude: json[r'longitude'] == null
? null
: num.parse(json[r'longitude'].toString()),
);
}
return null;

View file

@ -16,11 +16,6 @@ void main() {
// final instance = AssetBulkUpdateDto();
group('test AssetBulkUpdateDto', () {
// String dateTimeOriginal
test('to test the property `dateTimeOriginal`', () async {
// TODO
});
// List<String> ids (default value: const [])
test('to test the property `ids`', () async {
// TODO
@ -36,16 +31,6 @@ void main() {
// TODO
});
// num latitude
test('to test the property `latitude`', () async {
// TODO
});
// num longitude
test('to test the property `longitude`', () async {
// TODO
});
// bool removeParent
test('to test the property `removeParent`', () async {
// TODO

View file

@ -1,62 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetFaceResponseDto
void main() {
// final instance = AssetFaceResponseDto();
group('test AssetFaceResponseDto', () {
// int boundingBoxX1
test('to test the property `boundingBoxX1`', () async {
// TODO
});
// int boundingBoxX2
test('to test the property `boundingBoxX2`', () async {
// TODO
});
// int boundingBoxY1
test('to test the property `boundingBoxY1`', () async {
// TODO
});
// int boundingBoxY2
test('to test the property `boundingBoxY2`', () async {
// TODO
});
// String id
test('to test the property `id`', () async {
// TODO
});
// int imageHeight
test('to test the property `imageHeight`', () async {
// TODO
});
// int imageWidth
test('to test the property `imageWidth`', () async {
// TODO
});
// PersonResponseDto person
test('to test the property `person`', () async {
// TODO
});
});
}

View file

@ -1,27 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetFaceUpdateDto
void main() {
// final instance = AssetFaceUpdateDto();
group('test AssetFaceUpdateDto', () {
// List<AssetFaceUpdateItem> data (default value: const [])
test('to test the property `data`', () async {
// TODO
});
});
}

View file

@ -1,32 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetFaceUpdateItem
void main() {
// final instance = AssetFaceUpdateItem();
group('test AssetFaceUpdateItem', () {
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// String personId
test('to test the property `personId`', () async {
// TODO
});
});
}

View file

@ -1,57 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AssetFaceWithoutPersonResponseDto
void main() {
// final instance = AssetFaceWithoutPersonResponseDto();
group('test AssetFaceWithoutPersonResponseDto', () {
// int boundingBoxX1
test('to test the property `boundingBoxX1`', () async {
// TODO
});
// int boundingBoxX2
test('to test the property `boundingBoxX2`', () async {
// TODO
});
// int boundingBoxY1
test('to test the property `boundingBoxY1`', () async {
// TODO
});
// int boundingBoxY2
test('to test the property `boundingBoxY2`', () async {
// TODO
});
// String id
test('to test the property `id`', () async {
// TODO
});
// int imageHeight
test('to test the property `imageHeight`', () async {
// TODO
});
// int imageWidth
test('to test the property `imageWidth`', () async {
// TODO
});
});
}

View file

@ -127,7 +127,7 @@ void main() {
// TODO
});
// List<PersonWithFacesResponseDto> people (default value: const [])
// List<PersonResponseDto> people (default value: const [])
test('to test the property `people`', () async {
// TODO
});

View file

@ -26,6 +26,11 @@ void main() {
// TODO
});
// DateTime requestedAt
test('to test the property `requestedAt`', () async {
// TODO
});
});

View file

@ -1,31 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
/// tests for FaceApi
void main() {
// final instance = FaceApi();
group('tests for FaceApi', () {
//Future<List<AssetFaceResponseDto>> getFaces(String id) async
test('test getFaces', () async {
// TODO
});
//Future<PersonResponseDto> reassignFacesById(String id, FaceDto faceDto) async
test('test reassignFacesById', () async {
// TODO
});
});
}

View file

@ -1,27 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for FaceDto
void main() {
// final instance = FaceDto();
group('test FaceDto', () {
// String id
test('to test the property `id`', () async {
// TODO
});
});
}

View file

@ -17,11 +17,6 @@ void main() {
// final instance = PersonApi();
group('tests for PersonApi', () {
//Future<PersonResponseDto> createPerson() async
test('test createPerson', () async {
// TODO
});
//Future<PeopleResponseDto> getAllPeople({ bool withHidden }) async
test('test getAllPeople', () async {
// TODO
@ -52,11 +47,6 @@ void main() {
// TODO
});
//Future<List<PersonResponseDto>> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto) async
test('test reassignFaces', () async {
// TODO
});
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
test('test updatePeople', () async {
// TODO

View file

@ -1,52 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for PersonWithFacesResponseDto
void main() {
// final instance = PersonWithFacesResponseDto();
group('test PersonWithFacesResponseDto', () {
// DateTime birthDate
test('to test the property `birthDate`', () async {
// TODO
});
// List<AssetFaceWithoutPersonResponseDto> faces (default value: const [])
test('to test the property `faces`', () async {
// TODO
});
// String id
test('to test the property `id`', () async {
// TODO
});
// bool isHidden
test('to test the property `isHidden`', () async {
// TODO
});
// String name
test('to test the property `name`', () async {
// TODO
});
// String thumbnailPath
test('to test the property `thumbnailPath`', () async {
// TODO
});
});
}

View file

@ -16,11 +16,6 @@ void main() {
// final instance = UpdateAssetDto();
group('test UpdateAssetDto', () {
// String dateTimeOriginal
test('to test the property `dateTimeOriginal`', () async {
// TODO
});
// String description
test('to test the property `description`', () async {
// TODO
@ -36,16 +31,6 @@ void main() {
// TODO
});
// num latitude
test('to test the property `latitude`', () async {
// TODO
});
// num longitude
test('to test the property `longitude`', () async {
// TODO
});
});

View file

@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.89.0+113
version: 1.88.2+112
isar_version: &isar_version 3.1.0+1
environment:

View file

@ -1,131 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:timezone/data/latest.dart';
import 'package:timezone/timezone.dart';
ExifInfo makeExif({
DateTime? dateTimeOriginal,
String? timeZone,
}) {
return ExifInfo(
dateTimeOriginal: dateTimeOriginal,
timeZone: timeZone,
);
}
Asset makeAsset({
required String id,
required DateTime createdAt,
ExifInfo? exifInfo,
}) {
return Asset(
checksum: '',
localId: id,
remoteId: id,
ownerId: 1,
fileCreatedAt: createdAt,
fileModifiedAt: DateTime.now(),
updatedAt: DateTime.now(),
durationInSeconds: 0,
type: AssetType.image,
fileName: id,
isFavorite: false,
isArchived: false,
isTrashed: false,
stackCount: 0,
exifInfo: exifInfo,
);
}
void main() {
// Init Timezone DB
initializeTimeZones();
group("Returns local time and offset if no exifInfo", () {
test('returns createdAt directly if in local', () {
final createdAt = DateTime(2023, 12, 12, 12, 12, 12);
final a = makeAsset(id: '1', createdAt: createdAt);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
expect(dt, createdAt);
expect(tz, createdAt.timeZoneOffset);
});
test('returns createdAt in local if in utc', () {
final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12);
final a = makeAsset(id: '1', createdAt: createdAt);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
final localCreatedAt = createdAt.toLocal();
expect(dt, localCreatedAt);
expect(tz, localCreatedAt.timeZoneOffset);
});
});
group("Returns dateTimeOriginal", () {
test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () {
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
final e = makeExif(dateTimeOriginal: dateTimeOriginal);
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
final dateTimeInUTC = dateTimeOriginal.toUtc();
expect(dt, dateTimeInUTC);
expect(tz, dateTimeInUTC.timeZoneOffset);
});
test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone',
() {
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
final e = makeExif(
dateTimeOriginal: dateTimeOriginal,
timeZone: "#_#",
); // Invalid timezone
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
final dateTimeInUTC = dateTimeOriginal.toUtc();
expect(dt, dateTimeInUTC);
expect(tz, dateTimeInUTC.timeZoneOffset);
});
});
group("Returns adjusted time if timezone available", () {
test('With timezone as location', () {
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
const location = "Asia/Hong_Kong";
final e =
makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location);
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
final adjustedTime =
TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location));
expect(dt, adjustedTime);
expect(tz, adjustedTime.timeZoneOffset);
});
test('With timezone as offset', () {
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
const offset = "utc+08:00";
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset);
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
final location = getLocation("Asia/Hong_Kong");
final offsetFromLocation =
Duration(milliseconds: location.currentTimeZone.offset);
final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation);
// Adds the offset to the actual time and returns the offset separately
expect(dt, adjustedTime);
expect(tz, offsetFromLocation);
});
});
}

View file

@ -95,8 +95,11 @@ void main() {
makeAsset(checksum: "c", remoteId: "1-1"),
];
expect(db.assets.countSync(), 5);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c1, false);
expect(db.assets.countSync(), 5);
});
@ -112,8 +115,11 @@ void main() {
makeAsset(checksum: "g", remoteId: "3-1"),
];
expect(db.assets.countSync(), 5);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c1, true);
expect(db.assets.countSync(), 7);
});
@ -129,23 +135,35 @@ void main() {
makeAsset(checksum: "j", remoteId: "2-1d"),
];
expect(db.assets.countSync(), 5);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c1 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c1, true);
expect(db.assets.countSync(), 8);
final bool c2 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c2 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c2, false);
expect(db.assets.countSync(), 8);
remoteAssets.removeAt(4);
final bool c3 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c3 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c3, true);
expect(db.assets.countSync(), 7);
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
final bool c4 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
final bool c4 = await s.syncRemoteAssetsToDb(
owner,
_failDiff,
(u, n) => remoteAssets,
);
expect(c4, true);
expect(db.assets.countSync(), 9);
});
@ -161,8 +179,8 @@ void main() {
final List<String> toDelete = ["2-1", "1-1"];
final bool c = await s.syncRemoteAssetsToDb(
owner,
(user, since) async => (toUpsert, toDelete),
(user) => throw Exception(),
(user, since) async => (toUpsert, toDelete, null),
(user, n) => throw Exception(),
);
expect(c, true);
expect(db.assets.countSync(), 6);
@ -170,7 +188,10 @@ void main() {
});
}
Future<(List<Asset>?, List<String>?)> _failDiff(User user, DateTime time) =>
Future.value((null, null));
Future<(List<Asset>?, List<String>?, DateTime?)> _failDiff(
User user,
DateTime time,
) =>
Future.value((null, null, null));
class MockHashService extends Mock implements HashService {}

View file

@ -52,8 +52,7 @@
},
{
"groupName": "base-image",
"matchPackagePrefixes": ["ghcr.io/immich-app/base-server"],
"minimumReleaseAge": "0"
"matchPackagePrefixes": ["ghcr.io/immich-app/base-server"]
},
{
"matchDatasources": ["docker"],
@ -62,9 +61,6 @@
"versioning": "node"
}
],
"ignorePaths": [
"mobile/openapi/pubspec.yaml"
],
"ignoreDeps": [
"http",
"latlong2",

View file

@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20231201@sha256:4701c0c5920c78e73040dd2b74d22042ffce393f1a9d3453d90a0ecf81ff8649 as dev
FROM ghcr.io/immich-app/base-server-dev:20231125@sha256:f33b6eaf384e76ef3705a6e2cc76d276144ad6d3366b82f9b45b07d6a19285e2 as dev
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
@ -23,7 +23,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20231201@sha256:b8e86cf4c3cad872f54bab25a83f7503480049eea5c0ae36a8b8460b13cad3b5
FROM ghcr.io/immich-app/base-server-prod:20231125@sha256:a0e15f5bf87a97a79a399a5adffb5fe5befc18fb212e8341e744d958fe41e32a
WORKDIR /usr/src/app
ENV NODE_ENV=production
@ -37,7 +37,6 @@ COPY server/start*.sh ./
RUN npm link && npm cache clean --force
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
ENV PATH="${PATH}:/usr/src/app/bin"
VOLUME /usr/src/app/upload
EXPOSE 3001
ENTRYPOINT ["tini", "--", "/bin/sh"]

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