Compare commits

..

117 commits

Author SHA1 Message Date
Jonathan Jogenfors
8dac9ed053 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-29 09:34:34 +01:00
Jonathan Jogenfors
21c73baf30 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-23 10:45:25 +01:00
Jonathan Jogenfors
76a1abe7bd don't touch package lock 2023-11-23 10:45:20 +01:00
Jonathan Jogenfors
50627a0bb5 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-22 14:00:10 +01:00
Jonathan Jogenfors
1279d42e0b cApiTALiZaTiON 2023-11-22 13:59:54 +01:00
Jonathan Jogenfors
e9fc2b8ff7 version bump 2023-11-22 09:56:01 +01:00
Jonathan Jogenfors
7c2634b7cb Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-22 09:36:06 +01:00
Jonathan Jogenfors
fd1eb95220 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-21 23:25:51 +01:00
Jonathan Jogenfors
0bfb6ea235 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-21 21:51:12 +01:00
Jonathan Jogenfors
ac026f32cb wip use axios as upload 2023-11-21 21:37:03 +01:00
Jonathan Jogenfors
e35b9d51f2 fix lint 2023-11-21 12:15:38 +01:00
Jonathan Jogenfors
53657c09a5 bump dependencies 2023-11-21 12:11:29 +01:00
Jonathan Jogenfors
b2df2f59c4 add version option 2023-11-21 12:09:12 +01:00
Jonathan Jogenfors
35359585cb feat: allow single files as argument 2023-11-21 11:58:41 +01:00
Jonathan Jogenfors
d2ecb5ca4d remove form-data 2023-11-21 11:45:57 +01:00
Jonathan Jogenfors
3593bda2fe bump cli version to 2.0.4 2023-11-21 11:44:12 +01:00
Jonathan Jogenfors
daad3efb19 cleanup 2023-11-21 11:44:01 +01:00
Jonathan Jogenfors
fb69b59314 create missing dir 2023-11-21 11:34:40 +01:00
Jonathan Jogenfors
4a40a48c34 fix session service tests 2023-11-21 11:27:09 +01:00
Jonathan Jogenfors
7c6ee25f89 feat: allow configurable config dir 2023-11-21 10:00:51 +01:00
Jonathan Jogenfors
19f83f3c79 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-21 08:48:09 +01:00
Jonathan Jogenfors
ae3cb5adf6 test key login flow 2023-11-21 00:30:44 +01:00
Jonathan Jogenfors
8d4bc7f6a8 fix lint 2023-11-20 23:50:51 +01:00
Jonathan Jogenfors
f6889c4d45 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 23:48:28 +01:00
Jonathan Jogenfors
c7ed7877f5 check e2e code in lint 2023-11-20 23:48:23 +01:00
Jonathan Jogenfors
191e8fcd77 Merge branch 'chore/publish-npm-cli-dir' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 14:35:56 +01:00
Jonathan Jogenfors
f63bd80dce Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 14:35:48 +01:00
Jonathan Jogenfors
6a423433ed Merge branch 'main' of https://github.com/immich-app/immich into chore/publish-npm-cli-dir 2023-11-20 14:32:01 +01:00
Jonathan Jogenfors
507c7e963c chore: normalize the github url 2023-11-20 14:31:53 +01:00
Jonathan Jogenfors
ca381bd2ac chore: bump node setup to v4 2023-11-20 14:24:51 +01:00
Jonathan Jogenfors
f5df0bc6f2 chore: add repo url to npmjs 2023-11-20 14:15:39 +01:00
Jonathan Jogenfors
46d35c74c5 chore: set cli release working dir 2023-11-20 14:14:56 +01:00
Jonathan Jogenfors
c8d417ccda Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 14:05:28 +01:00
Jonathan Jogenfors
c124282514 chore: split cli e2e tests into one file per command 2023-11-20 13:56:01 +01:00
Jonathan Jogenfors
6421fcf2f8 assert on server version 2023-11-20 13:50:48 +01:00
Jonathan Jogenfors
eb939cd4f6 cleanup github test names 2023-11-20 13:50:37 +01:00
Jonathan Jogenfors
57ff53beb6 make github actions run makefile e2e tests 2023-11-20 12:57:52 +01:00
Jonathan Jogenfors
bbe6795e33 increase test timeout to 10 minutes 2023-11-20 12:56:10 +01:00
Jonathan Jogenfors
e1c3634490 use new e2e composes in makefile 2023-11-20 12:54:07 +01:00
Jonathan Jogenfors
1080d6a26e reshuffle docker compose files 2023-11-20 11:35:44 +01:00
Jonathan Jogenfors
cf5fb63759 Merge branch 'chore/push-to-npm' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 09:59:57 +01:00
Jonathan Jogenfors
d976140227 run npm ci in server 2023-11-20 09:37:23 +01:00
Jonathan Jogenfors
dfbabf5da7 remove state from e2e 2023-11-20 09:36:17 +01:00
Jonathan Jogenfors
8815d81d00 run npm ci in server 2023-11-20 08:57:31 +01:00
Jonathan Jogenfors
04954c5616 fix: server e2e 2023-11-20 08:54:33 +01:00
Jonathan Jogenfors
713b13cc57 Merge branches 'feat/cli-e2e' and 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-20 08:36:10 +01:00
Jason Rasmussen
92e4bbca23
chore(cli): push to npm 2023-11-19 19:55:55 -05:00
Jonathan Jogenfors
c5002693a8 chore: add npmignore 2023-11-20 00:11:07 +01:00
Jonathan Jogenfors
c45f68bc46 remove submodule 2023-11-19 23:59:38 +01:00
Jonathan Jogenfors
0388a8a79b Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-19 23:56:49 +01:00
Jonathan Jogenfors
6d3471b6f3 Merge branch 'feat/cli-albums' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-19 23:14:23 +01:00
Jonathan Jogenfors
b11d7668a6 fix: set correct cli date 2023-11-19 23:11:52 +01:00
Jonathan Jogenfors
ca97af6c12 feat: add cli v2 milestone 2023-11-19 23:08:51 +01:00
Jonathan Jogenfors
7257780ecd chore: fix docs links 2023-11-19 22:57:47 +01:00
Jonathan Jogenfors
74a336d1e6 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-19 22:38:33 +01:00
Jonathan Jogenfors
4e099cc8cb remove cli from dockerignore 2023-11-19 22:38:15 +01:00
Alex
afe1470965
Merge branch 'main' of github.com:immich-app/immich into feat/cli-albums 2023-11-19 10:06:16 -06:00
Jonathan Jogenfors
b00a7d63cf Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-19 00:32:29 +01:00
Jonathan Jogenfors
3ca69601a8 correct docker build 2023-11-19 00:28:14 +01:00
Jonathan Jogenfors
2981a383d2 set docker context 2023-11-17 23:47:37 +01:00
Jonathan Jogenfors
5ede8a8bcc run cli e2e in correct folder 2023-11-17 15:59:29 +01:00
Jonathan Jogenfors
3c8e3e6e8b fix cli docker file 2023-11-17 15:56:55 +01:00
Jonathan Jogenfors
6e4a1c10d0 cli docker e2e tests 2023-11-17 15:53:31 +01:00
Jonathan Jogenfors
ef3d2683d9 set node version 2023-11-17 13:16:11 +01:00
Jonathan Jogenfors
ec37247d95 increase test timeout 2023-11-17 13:10:43 +01:00
Jonathan Jogenfors
c776ef4f16 check out submodules too 2023-11-17 12:22:02 +01:00
Jonathan Jogenfors
0efd9a65b6 choose different working directory 2023-11-17 12:11:11 +01:00
Jonathan Jogenfors
b0b8ce2d6d run npm ci in server 2023-11-17 12:09:48 +01:00
Jonathan Jogenfors
7a2c27484c set matrix strategy in workflow 2023-11-17 12:07:30 +01:00
Jonathan Jogenfors
36e8397ce2 cleanup 2023-11-17 11:59:18 +01:00
Jonathan Jogenfors
9ba8565730 simplify e2e test 2023-11-17 11:47:50 +01:00
Jonathan Jogenfors
969b0b7367 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-17 11:16:53 +01:00
Jonathan Jogenfors
4c3a1ca168 can do e2e tests in the cli 2023-11-17 11:16:48 +01:00
Jonathan Jogenfors
11adf2e906 add cli e2e to github actions 2023-11-17 09:51:04 +01:00
Jonathan Jogenfors
5e56b65b38 git ignore for geocode in cli 2023-11-17 00:58:36 +01:00
Jonathan Jogenfors
ebd408e06a e2e test can start 2023-11-17 00:30:18 +01:00
Jonathan Jogenfors
f77b833eea Merge branch 'feat/cli-albums' of https://github.com/immich-app/immich into feat/cli-e2e 2023-11-17 00:19:36 +01:00
Jonathan Jogenfors
9ba35e9bbd Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-17 00:17:50 +01:00
Jonathan Jogenfors
448f5f9227 chore: remove unneeded packages 2023-11-17 00:17:41 +01:00
Jonathan Jogenfors
11c4051bd8 wip 2023-11-17 00:14:01 +01:00
Jonathan Jogenfors
67827e0929 chore: cleanup 2023-11-16 16:10:59 +01:00
Jonathan Jogenfors
1724eb6f6f feat: add more info to server-info command 2023-11-16 16:09:16 +01:00
Jonathan Jogenfors
a9cbd573f4 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-16 15:38:24 +01:00
Jonathan Jogenfors
6e04f41f1e chore: remove skipped asset field 2023-11-16 15:38:16 +01:00
Jonathan Jogenfors
4be93e5af8 feat: album name on windows 2023-11-16 14:45:20 +01:00
Jonathan Jogenfors
ef3a37726b fix: cleanup 2023-11-16 14:39:09 +01:00
Jonathan Jogenfors
60a622aa33 feat: use sdk 2023-11-16 14:35:50 +01:00
Jonathan Jogenfors
8ee629fd16 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-14 22:55:19 +01:00
Jonathan Jogenfors
927718fa32 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-14 09:06:26 +01:00
Jonathan Jogenfors
eee3b7f789 chore: add version number to cli 2023-11-13 23:52:32 +01:00
Jonathan Jogenfors
8471bf569e chore: rename ignore pattern 2023-11-13 23:36:58 +01:00
Jonathan Jogenfors
ad7ac83c2e Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-13 23:19:23 +01:00
Jonathan Jogenfors
97e2ea9607 docs: add cli documentation 2023-11-13 23:19:20 +01:00
Jonathan Jogenfors
b4ca48b246 chore: fix lint 2023-11-13 09:39:02 +01:00
Jonathan Jogenfors
7c40e36e35 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-13 09:23:01 +01:00
Jonathan Jogenfors
633def4955 chore: use crawl service from server 2023-11-13 09:22:58 +01:00
Jonathan Jogenfors
89993314b1 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-11 23:46:47 +01:00
Jonathan Jogenfors
1c4cd3eea4 feat: pull file formats from server 2023-11-11 23:46:29 +01:00
Jonathan Jogenfors
b438c6b514 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-11 23:23:06 +01:00
Jonathan Jogenfors
0d39474fc9 fix: typos 2023-11-11 00:51:02 +01:00
Jonathan Jogenfors
b8c5744e0f fix: add check step to cli 2023-11-11 00:49:55 +01:00
Jonathan Jogenfors
d68a9d2c5e feat: can create albums 2023-11-11 00:43:43 +01:00
Jonathan Jogenfors
02152082d1 fix: success message spacing 2023-11-10 21:14:51 +01:00
Jonathan Jogenfors
f962e8be42 docs: add compilation step to cli 2023-11-10 21:00:30 +01:00
Jonathan Jogenfors
39026072be Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-10 20:59:49 +01:00
Jonathan Jogenfors
e76c871740 docs: add todo for file format from server 2023-11-10 20:59:46 +01:00
Jonathan Jogenfors
015d8d8267 feat: add logout to cli 2023-11-10 20:59:16 +01:00
Jonathan Jogenfors
f588416659 chore: remove import functionality from cli 2023-11-10 10:16:59 +01:00
Jonathan Jogenfors
db28694f68 cleanup 2023-11-09 14:44:24 +01:00
Jonathan Jogenfors
06d78d2dac docs: add info on running without building 2023-11-09 12:03:20 +01:00
Jonathan Jogenfors
9d25a60455 feat: rewrite cli readme 2023-11-08 17:34:38 +01:00
Jonathan Jogenfors
55615bcd15 feat: use immich scoped package 2023-11-08 17:11:35 +01:00
Jonathan Jogenfors
b2bbdf423a Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-11-08 16:30:30 +01:00
Jonathan Jogenfors
a983eddb55 docs: remove cli folder 2023-10-11 11:48:35 +02:00
Jonathan Jogenfors
1b7c8a29cb feat: add format fix 2023-10-11 10:59:08 +02:00
Jonathan Jogenfors
4439bc2355 Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums 2023-10-11 10:46:57 +02:00
Jonathan Jogenfors
1d385b6cbe Allow building and installing cli 2023-10-04 23:56:12 +02:00
290 changed files with 1941 additions and 11352 deletions

View file

@ -1,5 +1,5 @@
.vscode/ .vscode/
cli/
design/ design/
docker/ docker/
docs/ docs/
@ -18,3 +18,8 @@ web/node_modules/
web/coverage/ web/coverage/
web/.svelte-kit web/.svelte-kit
web/build/ web/build/
cli/node_modules
cli/.reverse-geocoding-dump/
cli/upload/
cli/dist/

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/**/*.md linguist-generated=true
web/src/api/open-api/**/*.ts -diff -merge web/src/api/open-api/**/*.ts -diff -merge
web/src/api/open-api/**/*.ts linguist-generated=true web/src/api/open-api/**/*.ts linguist-generated=true
*.sh text eol=lf

View file

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

View file

@ -21,7 +21,7 @@ jobs:
submodules: "recursive" submodules: "recursive"
- name: Run e2e tests - name: Run e2e tests
run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build run: make test-server-e2e
doc-tests: doc-tests:
name: Docs name: Docs
@ -90,9 +90,14 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run npm install - name: Run npm install in cli
run: npm ci run: npm ci
- uses: chill-viking/npm-ci@latest
name: Run npm install in server
with:
working_directory: "server"
- name: Run linter - name: Run linter
run: npm run lint run: npm run lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
@ -109,6 +114,23 @@ jobs:
run: npm run test:cov run: npm run test:cov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-e2e-tests:
name: CLI (e2e)
strategy:
matrix:
os: [ubuntu-latest] # TODO: macos and windows
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Run e2e tests
run: make test-cli-e2e
web-unit-tests: web-unit-tests:
name: Web name: Web
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -209,11 +231,11 @@ jobs:
exit 1 exit 1
generated-typeorm-migrations-up-to-date: generated-typeorm-migrations-up-to-date:
name: TypeORM Checks name: TypeORM Migrations
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
postgres: postgres:
image: postgres@sha256:6dfee32131933ab4ca25a00360c3f427fdc134de56f9a90c6c9a4956b48aea85 image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@ -236,7 +258,7 @@ jobs:
- name: Install server dependencies - name: Install server dependencies
run: npm ci run: npm ci
- name: Build the app - name: Build the
run: npm run build run: npm run build
- name: Run existing migrations - name: Run existing migrations
@ -252,30 +274,13 @@ jobs:
with: with:
files: | files: |
server/src/infra/migrations/ 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' if: steps.verify-changed-files.outputs.files_changed == 'true'
run: | 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 }}" echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1 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: # mobile-integration-tests:
# name: Run mobile end-to-end integration tests # name: Run mobile end-to-end integration tests
# runs-on: macos-latest # runs-on: macos-latest

View file

@ -16,8 +16,11 @@ stage:
pull-stage: pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull docker compose -f ./docker/docker-compose.staging.yml pull
test-e2e: test-server-e2e:
docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build docker compose -f ./server/test/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
test-cli-e2e:
docker compose -f ./cli/test/docker-compose.cli-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-cli --remove-orphans --build
prod: prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@ -26,10 +29,7 @@ prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
api: api:
npm --prefix server run api:generate cd ./server && npm run api:generate
sql:
npm --prefix server run sql:generate
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh

View file

@ -11,7 +11,7 @@
<p align="center"> <p align="center">
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL"> <img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
</p> </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/> <br/>
<a href="https://immich.app"> <a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Haupt-Screenshot"> <img src="design/immich-screenshots.png" title="Haupt-Screenshot">
@ -32,10 +32,10 @@
## Warnung ## Warnung
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung. - ⚠️ Das Projekt befindet sich unter **sehr aktiver** Entwicklung.
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes. - ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.** - ⚠️ **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 ## Inhalt
@ -54,9 +54,9 @@ Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://im
## Demo ## 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" ```bash title="Demo Credential"
Die Anmeldedaten Die Anmeldedaten
@ -73,8 +73,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Funktionen | Mobil | Web | | Funktionen | Mobil | Web |
| ---------------------------------------------------- | ------ | ----- | | ---------------------------------------------------- | ------ | ----- |
| Fotos & Videos hochladen und ansehen | Ja | Ja | | Fotos & Videos hochladen und ansehen | Ja | Ja |
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. | | Automatisches Backup wenn die App offen ist | Ja | n. a. |
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. | | Selektive Auswahl von Alben zum Backup | Ja | n. a. |
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja | | Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
| Unterstützt mehrere Benutzer | Ja | Ja | | Unterstützt mehrere Benutzer | Ja | Ja |
| Album und geteilte Alben | 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 | | Unterstützt RAW Formate | Ja | Ja |
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja | | Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | 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. | | Backup im Hintergrund | Ja | n. a. |
| Virtuelles Scrollen | Ja | Ja | | Virtuelles Scrollen | Ja | Ja |
| OAuth Unterstützung | 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 ## 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. 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 ### Spenden
- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors - [Monatliche Spende](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors - [Einmalige Spende](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/) - [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502) - [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz - ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
## Mitwirkende ## Unterstützer
<a href="https://github.com/alextran1502/immich/graphs/contributors"> <a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/> <img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a> </a>

4
cli/.gitignore vendored
View file

@ -10,4 +10,6 @@ oclif.manifest.json
.vscode .vscode
.idea .idea
/coverage/ /coverage/
.reverse-geocoding-dump/
upload/

View file

@ -1,4 +1,6 @@
**/*.spec.js **/*.spec.js
test/**
upload/**
.editorconfig .editorconfig
.eslintignore .eslintignore
.eslintrc.js .eslintrc.js

19
cli/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
FROM ghcr.io/immich-app/base-server-dev:20231109 as test
WORKDIR /usr/src/app/server
COPY server/package.json server/package-lock.json ./
RUN npm ci
COPY ./server/ .
WORKDIR /usr/src/app/cli
COPY cli/package.json cli/package-lock.json ./
RUN npm ci
COPY ./cli/ .
FROM ghcr.io/immich-app/base-server-prod:20231109
VOLUME /usr/src/app/upload
EXPOSE 3001
ENTRYPOINT ["tini", "--", "/bin/sh"]

237
cli/package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.0.4", "version": "2.0.5",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.0.4", "version": "2.0.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
@ -37,6 +37,7 @@
"eslint-plugin-jest": "^27.2.2", "eslint-plugin-jest": "^27.2.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^49.0.0", "eslint-plugin-unicorn": "^49.0.0",
"immich": "file:../server",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-extended": "^4.0.0", "jest-extended": "^4.0.0",
"jest-message-util": "^29.5.0", "jest-message-util": "^29.5.0",
@ -49,6 +50,106 @@
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
}, },
"../server": {
"version": "1.88.2",
"dev": true,
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",
"@immich/cli": "^2.0.3",
"@nestjs/bullmq": "^10.0.1",
"@nestjs/common": "^10.2.2",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.2.2",
"@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^3.0.3",
"@nestjs/swagger": "^7.1.8",
"@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2",
"@socket.io/redis-adapter": "^8.2.1",
"archiver": "^6.0.0",
"axios": "^1.5.0",
"bcrypt": "^5.1.1",
"bullmq": "^4.8.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "~23.5.0",
"exiftool-vendored.pl": "12.70",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^7.0.7",
"glob": "^10.3.3",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
"local-reverse-geocoder": "0.16.5",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
"mv": "^2.1.1",
"nest-commander": "^3.11.1",
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
"sharp": "^0.32.6",
"thumbhash": "^0.1.1",
"typeorm": "^0.3.17",
"typesense": "^1.7.1",
"ua-parser-js": "^1.0.35"
},
"bin": {
"immich": "bin/cli.sh",
"immich-admin": "bin/admin-cli.sh"
},
"devDependencies": {
"@nestjs/cli": "^10.1.16",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.2",
"@openapitools/openapi-generator-cli": "2.7.0",
"@testcontainers/postgresql": "^10.2.1",
"@types/archiver": "^6.0.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/jest": "29.5.9",
"@types/jest-when": "^3.5.2",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/mv": "^2.1.2",
"@types/node": "^20.5.7",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"dotenv": "^16.3.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.4",
"jest-when": "^3.6.0",
"mock-fs": "^5.2.0",
"prettier": "^3.0.2",
"prettier-plugin-organize-imports": "^3.2.3",
"rimraf": "^5.0.1",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"testcontainers": "^10.2.1",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2",
"utimes": "^5.2.1"
}
},
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
@ -1519,9 +1620,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/chai": { "node_modules/@types/chai": {
"version": "4.3.11", "version": "4.3.10",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
"dev": true "dev": true
}, },
"node_modules/@types/cli-progress": { "node_modules/@types/cli-progress": {
@ -1567,9 +1668,9 @@
} }
}, },
"node_modules/@types/jest": { "node_modules/@types/jest": {
"version": "29.5.10", "version": "29.5.8",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
"integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"expect": "^29.0.0", "expect": "^29.0.0",
@ -1604,9 +1705,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.10.1", "version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@ -3891,6 +3992,10 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immich": {
"resolved": "../server",
"link": true
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -7905,9 +8010,9 @@
"dev": true "dev": true
}, },
"@types/chai": { "@types/chai": {
"version": "4.3.11", "version": "4.3.10",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
"dev": true "dev": true
}, },
"@types/cli-progress": { "@types/cli-progress": {
@ -7953,9 +8058,9 @@
} }
}, },
"@types/jest": { "@types/jest": {
"version": "29.5.10", "version": "29.5.8",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
"integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
"dev": true, "dev": true,
"requires": { "requires": {
"expect": "^29.0.0", "expect": "^29.0.0",
@ -7990,9 +8095,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "20.10.1", "version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
"dev": true, "dev": true,
"requires": { "requires": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@ -9547,6 +9652,98 @@
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true "dev": true
}, },
"immich": {
"version": "file:../server",
"requires": {
"@babel/runtime": "^7.22.11",
"@immich/cli": "^2.0.3",
"@nestjs/bullmq": "^10.0.1",
"@nestjs/cli": "^10.1.16",
"@nestjs/common": "^10.2.2",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.2.2",
"@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^3.0.3",
"@nestjs/schematics": "^10.0.2",
"@nestjs/swagger": "^7.1.8",
"@nestjs/testing": "^10.2.2",
"@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2",
"@openapitools/openapi-generator-cli": "2.7.0",
"@socket.io/redis-adapter": "^8.2.1",
"@testcontainers/postgresql": "^10.2.1",
"@types/archiver": "^6.0.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/jest": "29.5.9",
"@types/jest-when": "^3.5.2",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/mv": "^2.1.2",
"@types/node": "^20.5.7",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"archiver": "^6.0.0",
"axios": "^1.5.0",
"bcrypt": "^5.1.1",
"bullmq": "^4.8.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"exiftool-vendored": "~23.5.0",
"exiftool-vendored.pl": "12.70",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^7.0.7",
"glob": "^10.3.3",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"jest": "^29.6.4",
"jest-when": "^3.6.0",
"joi": "^17.10.0",
"local-reverse-geocoder": "0.16.5",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
"mock-fs": "^5.2.0",
"mv": "^2.1.1",
"nest-commander": "^3.11.1",
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"prettier": "^3.0.2",
"prettier-plugin-organize-imports": "^3.2.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.1",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
"sharp": "^0.32.6",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"testcontainers": "^10.2.1",
"thumbhash": "^0.1.1",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typeorm": "^0.3.17",
"typescript": "^5.2.2",
"typesense": "^1.7.1",
"ua-parser-js": "^1.0.35",
"utimes": "^5.2.1"
}
},
"import-fresh": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.0.4", "version": "2.0.5",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@ -35,6 +35,7 @@
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2", "eslint-plugin-jest": "^27.2.2",
"immich": "file:../server",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^49.0.0", "eslint-plugin-unicorn": "^49.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
@ -50,13 +51,15 @@
}, },
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"prepack": "npm run build", "prepack": "npm run build",
"test": "jest", "test": "jest",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"format": "prettier --check .", "format": "prettier --check .",
"format:fix": "prettier --write .", "format:fix": "prettier --write .",
"check": "tsc --noEmit" "check": "tsc --noEmit",
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand"
}, },
"jest": { "jest": {
"clearMocks": true, "clearMocks": true,
@ -71,10 +74,15 @@
"^.+\\.ts$": "ts-jest" "^.+\\.ts$": "ts-jest"
}, },
"collectCoverageFrom": [ "collectCoverageFrom": [
"<rootDir>/src/**/*.(t|j)s" "<rootDir>/src/**/*.(t|j)s",
"!**/open-api/**"
], ],
"moduleNameMapper": { "moduleNameMapper": {
"^@api(|/.*)$": "<rootDir>/src/api/$1" "^@api(|/.*)$": "<rootDir>/src/api/$1",
"^@test(|/.*)$": "<rootDir>../server/test/$1",
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
"^@app/domain(|/.*)$": "<rootDir>../server/src/domain/$1"
}, },
"coverageDirectory": "./coverage", "coverageDirectory": "./coverage",
"testEnvironment": "node" "testEnvironment": "node"

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -447,12 +447,6 @@ export interface AssetBulkDeleteDto {
* @interface AssetBulkUpdateDto * @interface AssetBulkUpdateDto
*/ */
export interface AssetBulkUpdateDto { export interface AssetBulkUpdateDto {
/**
*
* @type {string}
* @memberof AssetBulkUpdateDto
*/
'dateTimeOriginal'?: string;
/** /**
* *
* @type {Array<string>} * @type {Array<string>}
@ -471,18 +465,6 @@ export interface AssetBulkUpdateDto {
* @memberof AssetBulkUpdateDto * @memberof AssetBulkUpdateDto
*/ */
'isFavorite'?: boolean; 'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'longitude'?: number;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -586,142 +568,6 @@ export const AssetBulkUploadCheckResultReasonEnum = {
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof 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 * @export
@ -978,10 +824,10 @@ export interface AssetResponseDto {
'ownerId': string; 'ownerId': string;
/** /**
* *
* @type {Array<PersonWithFacesResponseDto>} * @type {Array<PersonResponseDto>}
* @memberof AssetResponseDto * @memberof AssetResponseDto
*/ */
'people'?: Array<PersonWithFacesResponseDto>; 'people'?: Array<PersonResponseDto>;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -1808,19 +1654,6 @@ export interface ExifResponseDto {
*/ */
'timeZone'?: string | null; 'timeZone'?: string | null;
} }
/**
*
* @export
* @interface FaceDto
*/
export interface FaceDto {
/**
*
* @type {string}
* @memberof FaceDto
*/
'id': string;
}
/** /**
* *
* @export * @export
@ -1934,8 +1767,7 @@ export const JobCommand = {
Start: 'start', Start: 'start',
Pause: 'pause', Pause: 'pause',
Resume: 'resume', Resume: 'resume',
Empty: 'empty', Empty: 'empty'
ClearFailed: 'clear-failed'
} as const; } as const;
export type JobCommand = typeof JobCommand[keyof typeof JobCommand]; export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
@ -2713,49 +2545,6 @@ export interface PersonUpdateDto {
*/ */
'name'?: string; '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 * @export
@ -4348,12 +4137,6 @@ export interface UpdateAlbumDto {
* @interface UpdateAssetDto * @interface UpdateAssetDto
*/ */
export interface UpdateAssetDto { export interface UpdateAssetDto {
/**
*
* @type {string}
* @memberof UpdateAssetDto
*/
'dateTimeOriginal'?: string;
/** /**
* *
* @type {string} * @type {string}
@ -4372,18 +4155,6 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto * @memberof UpdateAssetDto
*/ */
'isFavorite'?: boolean; 'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'longitude'?: number;
} }
/** /**
* *
@ -11541,233 +11312,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 * JobApi - axios parameter creator
* @export * @export
@ -13599,44 +13143,6 @@ export class PartnerApi extends BaseAPI {
*/ */
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) { export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
return { 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] * @param {boolean} [withHidden]
@ -13896,54 +13402,6 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
options: localVarRequestOptions, 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 * @param {PeopleUpdateDto} peopleUpdateDto
@ -14046,15 +13504,6 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
export const PersonApiFp = function(configuration?: Configuration) { export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
return { 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] * @param {boolean} [withHidden]
@ -14116,17 +13565,6 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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 * @param {PeopleUpdateDto} peopleUpdateDto
@ -14158,14 +13596,6 @@ export const PersonApiFp = function(configuration?: Configuration) {
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = PersonApiFp(configuration) const localVarFp = PersonApiFp(configuration)
return { 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. * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -14220,15 +13650,6 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> { mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath)); 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. * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -14341,27 +13762,6 @@ export interface PersonApiMergePersonRequest {
readonly mergePersonDto: MergePersonDto 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. * Request parameters for updatePeople operation in PersonApi.
* @export * @export
@ -14404,16 +13804,6 @@ export interface PersonApiUpdatePersonRequest {
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class PersonApi 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. * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -14480,17 +13870,6 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath)); 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. * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,9 @@
import { ImmichApi } from '../api/client'; import { ImmichApi } from '../api/client';
import path from 'node:path';
import { SessionService } from '../services/session.service'; import { SessionService } from '../services/session.service';
import { LoginError } from '../cores/errors/login-error'; import { LoginError } from '../cores/errors/login-error';
import { exit } from 'node:process'; import { exit } from 'node:process';
import os from 'os';
import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api'; import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
export abstract class BaseCommand { export abstract class BaseCommand {
protected sessionService!: SessionService; protected sessionService!: SessionService;
@ -12,14 +11,11 @@ export abstract class BaseCommand {
protected user!: UserResponseDto; protected user!: UserResponseDto;
protected serverVersion!: ServerVersionResponseDto; protected serverVersion!: ServerVersionResponseDto;
protected configDir; constructor(options: BaseOptionsDto) {
protected authPath; if (!options.config) {
throw new Error('Config directory is required');
constructor() { }
const userHomeDir = os.homedir(); this.sessionService = new SessionService(options.config);
this.configDir = path.join(userHomeDir, '.config/immich/');
this.sessionService = new SessionService(this.configDir);
this.authPath = path.join(this.configDir, 'auth.yml');
} }
public async connect(): Promise<void> { public async connect(): Promise<void> {

View file

@ -2,7 +2,7 @@ import { Asset } from '../cores/models/asset';
import { CrawlService } from '../services'; import { CrawlService } from '../services';
import { UploadOptionsDto } from '../cores/dto/upload-options-dto'; import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto'; import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
import fs from 'node:fs';
import cliProgress from 'cli-progress'; import cliProgress from 'cli-progress';
import byteSize from 'byte-size'; import byteSize from 'byte-size';
import { BaseCommand } from '../cli/base-command'; import { BaseCommand } from '../cli/base-command';
@ -15,8 +15,6 @@ export default class Upload extends BaseCommand {
public async run(paths: string[], options: UploadOptionsDto): Promise<void> { public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
await this.connect(); await this.connect();
const deviceId = 'CLI';
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video); const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
@ -25,14 +23,26 @@ export default class Upload extends BaseCommand {
crawlOptions.recursive = options.recursive; crawlOptions.recursive = options.recursive;
crawlOptions.exclusionPatterns = options.exclusionPatterns; crawlOptions.exclusionPatterns = options.exclusionPatterns;
const files: string[] = [];
for (const pathArgument of paths) {
const fileStat = await fs.promises.lstat(pathArgument);
if (fileStat.isFile()) {
files.push(pathArgument);
}
}
const crawledFiles: string[] = await crawlService.crawl(crawlOptions); const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
crawledFiles.push(...files);
if (crawledFiles.length === 0) { if (crawledFiles.length === 0) {
console.log('No assets found, exiting'); console.log('No assets found, exiting');
return; return;
} }
const assetsToUpload = crawledFiles.map((path) => new Asset(path, deviceId)); const assetsToUpload = crawledFiles.map((path) => new Asset(path));
const uploadProgress = new cliProgress.SingleBar( const uploadProgress = new cliProgress.SingleBar(
{ {

37
cli/src/constants.ts Normal file
View file

@ -0,0 +1,37 @@
import pkg from '../package.json';
export interface ICLIVersion {
major: number;
minor: number;
patch: number;
}
export class CLIVersion implements ICLIVersion {
constructor(
public readonly major: number,
public readonly minor: number,
public readonly patch: number,
) {}
toString() {
return `${this.major}.${this.minor}.${this.patch}`;
}
toJSON() {
const { major, minor, patch } = this;
return { major, minor, patch };
}
static fromString(version: string): CLIVersion {
const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex);
if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number);
return new CLIVersion(major, minor, patch);
} else {
throw new Error(`Invalid version format: ${version}`);
}
}
}
export const cliVersion = CLIVersion.fromString(pkg.version);

View file

@ -0,0 +1,3 @@
export class BaseOptionsDto {
config?: string;
}

View file

@ -1,9 +1,8 @@
export class UploadOptionsDto { export class UploadOptionsDto {
recursive = false; recursive? = false;
exclusionPatterns!: string[]; exclusionPatterns?: string[] = [];
dryRun = false; dryRun? = false;
skipHash = false; skipHash? = false;
delete = false; delete? = false;
readOnly = true; album? = false;
album = false;
} }

View file

@ -2,10 +2,8 @@ export class LoginError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
// assign the error class name in your custom error (as a shortcut)
this.name = this.constructor.name; this.name = this.constructor.name;
// capturing the stack trace keeps the reference to your error class
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
} }
} }

View file

@ -17,9 +17,8 @@ export class Asset {
fileSize!: number; fileSize!: number;
albumName?: string; albumName?: string;
constructor(path: string, deviceId: string) { constructor(path: string) {
this.path = path; this.path = path;
this.deviceId = deviceId;
} }
async process() { async process() {
@ -45,12 +44,11 @@ export class Asset {
if (!this.deviceAssetId) throw new Error('Device asset id not set'); if (!this.deviceAssetId) throw new Error('Device asset id not set');
if (!this.fileCreatedAt) throw new Error('File created at not set'); if (!this.fileCreatedAt) throw new Error('File created at not set');
if (!this.fileModifiedAt) throw new Error('File modified at not set'); if (!this.fileModifiedAt) throw new Error('File modified at not set');
if (!this.deviceId) throw new Error('Device id not set');
const data: any = { const data: any = {
assetData: this.assetData as any, assetData: this.assetData as any,
deviceAssetId: this.deviceAssetId, deviceAssetId: this.deviceAssetId,
deviceId: this.deviceId, deviceId: 'CLI',
fileCreatedAt: this.fileCreatedAt, fileCreatedAt: this.fileCreatedAt,
fileModifiedAt: this.fileModifiedAt, fileModifiedAt: this.fileModifiedAt,
isFavorite: String(false), isFavorite: String(false),

View file

@ -1,13 +1,23 @@
#! /usr/bin/env node #! /usr/bin/env node
import { program, Option } from 'commander'; import { Option, Command } from 'commander';
import Upload from './commands/upload'; import Upload from './commands/upload';
import ServerInfo from './commands/server-info'; import ServerInfo from './commands/server-info';
import LoginKey from './commands/login/key'; import LoginKey from './commands/login/key';
import Logout from './commands/logout'; import Logout from './commands/logout';
import { version } from '../package.json'; import { version } from '../package.json';
program.name('immich').description('Immich command line interface').version(version); import path from 'node:path';
import os from 'os';
const userHomeDir = os.homedir();
const configDir = path.join(userHomeDir, '.config/immich/');
const program = new Command()
.name('immich')
.version(version)
.description('Command line interface for Immich')
.addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir));
program program
.command('upload') .command('upload')
@ -30,14 +40,14 @@ program
.argument('[paths...]', 'One or more paths to assets to be uploaded') .argument('[paths...]', 'One or more paths to assets to be uploaded')
.action(async (paths, options) => { .action(async (paths, options) => {
options.exclusionPatterns = options.ignore; options.exclusionPatterns = options.ignore;
await new Upload().run(paths, options); await new Upload(program.opts()).run(paths, options);
}); });
program program
.command('server-info') .command('server-info')
.description('Display server information') .description('Display server information')
.action(async () => { .action(async () => {
await new ServerInfo().run(); await new ServerInfo(program.opts()).run();
}); });
program program
@ -46,14 +56,14 @@ program
.argument('[instanceUrl]') .argument('[instanceUrl]')
.argument('[apiKey]') .argument('[apiKey]')
.action(async (paths, options) => { .action(async (paths, options) => {
await new LoginKey().run(paths, options); await new LoginKey(program.opts()).run(paths, options);
}); });
program program
.command('logout') .command('logout')
.description('Remove stored credentials') .description('Remove stored credentials')
.action(async () => { .action(async () => {
await new Logout().run(); await new Logout(program.opts()).run();
}); });
program.parse(process.argv); program.parse(process.argv);

View file

@ -1,8 +1,17 @@
import { SessionService } from './session.service'; import { SessionService } from './session.service';
import mockfs from 'mock-fs';
import fs from 'node:fs'; import fs from 'node:fs';
import yaml from 'yaml'; import yaml from 'yaml';
import { LoginError } from '../cores/errors/login-error'; import { LoginError } from '../cores/errors/login-error';
import {
TEST_AUTH_FILE,
TEST_CONFIG_DIR,
TEST_IMMICH_API_KEY,
TEST_IMMICH_INSTANCE_URL,
createTestAuthFile,
deleteAuthFile,
readTestAuthFile,
spyOnConsole,
} from '../../test/cli-test-utils';
const mockPingServer = jest.fn(() => Promise.resolve({ data: { res: 'pong' } })); const mockPingServer = jest.fn(() => Promise.resolve({ data: { res: 'pong' } }));
const mockUserInfo = jest.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } })); const mockUserInfo = jest.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } }));
@ -22,74 +31,85 @@ jest.mock('../api/open-api', () => {
describe('SessionService', () => { describe('SessionService', () => {
let sessionService: SessionService; let sessionService: SessionService;
let consoleSpy: jest.SpyInstance;
beforeAll(() => { beforeAll(() => {
// Write a dummy output before mock-fs to prevent some annoying errors consoleSpy = spyOnConsole();
console.log();
}); });
beforeEach(() => { beforeEach(() => {
const configDir = '/config'; deleteAuthFile();
sessionService = new SessionService(configDir); sessionService = new SessionService(TEST_CONFIG_DIR);
});
afterEach(() => {
deleteAuthFile();
}); });
it('should connect to immich', async () => { it('should connect to immich', async () => {
mockfs({ await createTestAuthFile(
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api', JSON.stringify({
}); apiKey: TEST_IMMICH_API_KEY,
instanceUrl: TEST_IMMICH_INSTANCE_URL,
}),
);
await sessionService.connect(); await sessionService.connect();
expect(mockPingServer).toHaveBeenCalledTimes(1); expect(mockPingServer).toHaveBeenCalledTimes(1);
}); });
it('should error if no auth file exists', async () => { it('should error if no auth file exists', async () => {
mockfs();
await sessionService.connect().catch((error) => { await sessionService.connect().catch((error) => {
expect(error.message).toEqual('No auth file exist. Please login first'); expect(error.message).toEqual('No auth file exist. Please login first');
}); });
}); });
it('should error if auth file is missing instance URl', async () => { it('should error if auth file is missing instance URl', async () => {
mockfs({ await createTestAuthFile(
'/config/auth.yml': 'foo: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\napiKey: https://test/api', JSON.stringify({
}); apiKey: TEST_IMMICH_API_KEY,
}),
);
await sessionService.connect().catch((error) => { await sessionService.connect().catch((error) => {
expect(error).toBeInstanceOf(LoginError); expect(error).toBeInstanceOf(LoginError);
expect(error.message).toEqual('Instance URL missing in auth config file /config/auth.yml'); expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`);
}); });
}); });
it('should error if auth file is missing api key', async () => { it('should error if auth file is missing api key', async () => {
mockfs({ await createTestAuthFile(
'/config/auth.yml': 'instanceUrl: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\nbar: https://test/api', JSON.stringify({
}); instanceUrl: TEST_IMMICH_INSTANCE_URL,
await sessionService.connect().catch((error) => { }),
expect(error).toBeInstanceOf(LoginError); );
expect(error.message).toEqual('API key missing in auth config file /config/auth.yml');
}); await expect(sessionService.connect()).rejects.toThrow(
new LoginError(`API key missing in auth config file ${TEST_AUTH_FILE}`),
);
}); });
it.skip('should create auth file when logged in', async () => { it('should create auth file when logged in', async () => {
mockfs(); await sessionService.keyLogin(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY);
await sessionService.keyLogin('https://test/api', 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg'); const data: string = await readTestAuthFile();
const data: string = await fs.promises.readFile('/config/auth.yml', 'utf8');
const authConfig = yaml.parse(data); const authConfig = yaml.parse(data);
expect(authConfig.instanceUrl).toBe('https://test/api'); expect(authConfig.instanceUrl).toBe(TEST_IMMICH_INSTANCE_URL);
expect(authConfig.apiKey).toBe('pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg'); expect(authConfig.apiKey).toBe(TEST_IMMICH_API_KEY);
}); });
it('should delete auth file when logging out', async () => { it('should delete auth file when logging out', async () => {
mockfs({ await createTestAuthFile(
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api', JSON.stringify({
}); apiKey: TEST_IMMICH_API_KEY,
instanceUrl: TEST_IMMICH_INSTANCE_URL,
}),
);
await sessionService.logout(); await sessionService.logout();
await fs.promises.access('/auth.yml', fs.constants.F_OK).catch((error) => { await fs.promises.access(TEST_AUTH_FILE, fs.constants.F_OK).catch((error) => {
expect(error.message).toContain('ENOENT'); expect(error.message).toContain('ENOENT');
}); });
});
afterEach(() => { expect(consoleSpy.mock.calls).toEqual([[`Removed auth file ${TEST_AUTH_FILE}`]]);
mockfs.restore();
}); });
}); });

View file

@ -5,33 +5,39 @@ import { ImmichApi } from '../api/client';
import { LoginError } from '../cores/errors/login-error'; import { LoginError } from '../cores/errors/login-error';
export class SessionService { export class SessionService {
readonly configDir: string; readonly configDir!: string;
readonly authPath!: string; readonly authPath!: string;
private api!: ImmichApi; private api!: ImmichApi;
constructor(configDir: string) { constructor(configDir: string) {
this.configDir = configDir; this.configDir = configDir;
this.authPath = path.join(this.configDir, 'auth.yml'); this.authPath = path.join(configDir, '/auth.yml');
} }
public async connect(): Promise<ImmichApi> { public async connect(): Promise<ImmichApi> {
await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => { let instanceUrl = process.env.IMMICH_INSTANCE_URL;
if (error.code === 'ENOENT') { let apiKey = process.env.IMMICH_API_KEY;
throw new LoginError('No auth file exist. Please login first');
if (!instanceUrl || !apiKey) {
await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => {
if (error.code === 'ENOENT') {
throw new LoginError('No auth file exist. Please login first');
}
});
const data: string = await fs.promises.readFile(this.authPath, 'utf8');
const parsedConfig = yaml.parse(data);
instanceUrl = parsedConfig.instanceUrl;
apiKey = parsedConfig.apiKey;
if (!instanceUrl) {
throw new LoginError(`Instance URL missing in auth config file ${this.authPath}`);
} }
});
const data: string = await fs.promises.readFile(this.authPath, 'utf8'); if (!apiKey) {
const parsedConfig = yaml.parse(data); throw new LoginError(`API key missing in auth config file ${this.authPath}`);
const instanceUrl: string = parsedConfig.instanceUrl; }
const apiKey: string = parsedConfig.apiKey;
if (!instanceUrl) {
throw new LoginError('Instance URL missing in auth config file ' + this.authPath);
}
if (!apiKey) {
throw new LoginError('API key missing in auth config file ' + this.authPath);
} }
this.api = new ImmichApi(instanceUrl, apiKey); this.api = new ImmichApi(instanceUrl, apiKey);
@ -59,10 +65,6 @@ export class SessionService {
} }
} }
if (!fs.existsSync(this.configDir)) {
console.error('waah');
}
fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey })); fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));
console.log('Wrote auth info to ' + this.authPath); console.log('Wrote auth info to ' + this.authPath);
@ -82,7 +84,7 @@ export class SessionService {
}); });
if (pingResponse.res !== 'pong') { if (pingResponse.res !== 'pong') {
throw new Error('Unexpected ping reply'); throw new Error(`Could not parse response. Is Immich listening on ${this.api.apiConfiguration.instanceUrl}?`);
} }
} }
} }

View file

@ -0,0 +1,38 @@
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
import fs from 'node:fs';
import path from 'node:path';
export const TEST_CONFIG_DIR = '/tmp/immich/';
export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
export const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
export const CLI_BASE_OPTIONS: BaseOptionsDto = { config: TEST_CONFIG_DIR };
export const spyOnConsole = () => jest.spyOn(console, 'log').mockImplementation();
export const createTestAuthFile = async (contents: string) => {
if (!fs.existsSync(TEST_CONFIG_DIR)) {
// Create config folder if it doesn't exist
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
if (!created) {
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
}
}
fs.writeFileSync(TEST_AUTH_FILE, contents);
};
export const readTestAuthFile = async (): Promise<string> => {
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
};
export const deleteAuthFile = () => {
try {
fs.unlinkSync(TEST_AUTH_FILE);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
};

View file

@ -0,0 +1,31 @@
version: '3.8'
name: 'immich-test-cli-e2e'
services:
immich-cli:
image: immich-cli-dev:latest
build:
context: ../../
dockerfile: cli/Dockerfile
target: test
working_dir: /usr/src/app/cli
command: npm run test:e2e
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=e2e_test
- IMMICH_RUN_ALL_TESTS=true
depends_on:
- database
database:
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
command: -c fsync=off
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: e2e_test
logging:
driver: none

View file

@ -0,0 +1,24 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"modulePaths": ["<rootDir>"],
"rootDir": "../..",
"globalSetup": "<rootDir>/../server/test/e2e/setup.ts",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"testTimeout": 6000000,
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"<rootDir>/src/**/*.(t|j)s",
"!<rootDir>/src/**/*.spec.(t|s)s",
"!<rootDir>/src/infra/migrations/**"
],
"coverageDirectory": "./coverage",
"moduleNameMapper": {
"^@test(|/.*)$": "<rootDir>../server/test/$1",
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
"^@app/domain(|/.*)$": "<rootDir>../server/src/domain/$1"
}
}

View file

@ -0,0 +1,48 @@
import { api } from '@test/api';
import { restoreTempFolder, testApp } from 'immich/test/test-utils';
import { LoginResponseDto } from 'src/api/open-api';
import { APIKeyCreateResponseDto } from '@app/domain';
import LoginKey from 'src/commands/login/key';
import { LoginError } from 'src/cores/errors/login-error';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`login-key (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
let instanceUrl: string;
spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
if (!process.env.IMMICH_INSTANCE_URL) {
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
} else {
instanceUrl = process.env.IMMICH_INSTANCE_URL;
}
});
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
});
beforeEach(async () => {
await testApp.reset();
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
});
it('should error when providing an invalid API key', async () => {
await expect(async () => await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
new LoginError(`Failed to connect to server ${instanceUrl}: Request failed with status code 401`),
);
});
it('should log in when providing the correct API key', async () => {
await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, apiKey.secret);
});
});

View file

@ -0,0 +1,42 @@
import { api } from '@test/api';
import { restoreTempFolder, testApp } from 'immich/test/test-utils';
import { LoginResponseDto } from 'src/api/open-api';
import ServerInfo from 'src/commands/server-info';
import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`server-info (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
const consoleSpy = spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
});
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
});
beforeEach(async () => {
await testApp.reset();
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
});
it('should show server version', async () => {
await new ServerInfo(CLI_BASE_OPTIONS).run();
expect(consoleSpy.mock.calls).toEqual([
[expect.stringMatching(new RegExp('Server is running version \\d+.\\d+.\\d+'))],
[expect.stringMatching('Supported image types: .*')],
[expect.stringMatching('Supported video types: .*')],
['Images: 0, Videos: 0, Total: 0'],
]);
});
});

View file

@ -0,0 +1,46 @@
import { api } from '@test/api';
import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from 'immich/test/test-utils';
import { LoginResponseDto } from 'src/api/open-api';
import Upload from 'src/commands/upload';
import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`upload (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
});
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
});
beforeEach(async () => {
await testApp.reset();
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
process.env.IMMICH_API_KEY = apiKey.secret;
});
it('should upload a folder recursively', async () => {
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets.length).toBeGreaterThan(4);
});
it('should create album from folder name', async () => {
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true,
album: true,
});
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(1);
});
});

3
cli/test/global-setup.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};

View file

@ -8,7 +8,7 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"target": "es2022", "target": "es2021",
"moduleResolution": "node16", "moduleResolution": "node16",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
@ -17,8 +17,14 @@
"esModuleInterop": true, "esModuleInterop": true,
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@test": ["test"], "@test": ["../server/test"],
"@test/*": ["test/*"] "@test/*": ["../server/test/*"],
"@app/immich": ["../server/src/immich"],
"@app/immich/*": ["../server/src/immich/*"],
"@app/infra": ["../server/src/infra"],
"@app/infra/*": ["../server/src/infra/*"],
"@app/domain": ["../server/src/domain"],
"@app/domain/*": ["../server/src/domain/*"]
} }
}, },
"exclude": ["dist", "node_modules", "upload"] "exclude": ["dist", "node_modules", "upload"]

View file

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

View file

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

View file

@ -69,12 +69,12 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46 image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07 image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
env_file: env_file:
- .env - .env
environment: 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`. 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" ```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 :::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: 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" ```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 :::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 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
COPY poetry.lock pyproject.toml ./ COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
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/* 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 \ ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \ TRANSFORMERS_CACHE=/cache \

View file

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

View file

@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 113, "android.injected.version.code" => 112,
"android.injected.version.name" => "1.89.0", "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') 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 distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80 distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2

View file

@ -144,8 +144,6 @@
"control_bottom_app_bar_stack": "Stack", "control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_unarchive": "Unarchive", "control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_upload": "Upload", "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_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create", "create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Share", "create_shared_album_page_share": "Share",
@ -167,7 +165,6 @@
"exif_bottom_sheet_description": "Add Description...", "exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION", "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_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_subtitle": "Use at your own risk!",
@ -464,18 +461,5 @@
"viewer_remove_from_stack": "Remove from Stack", "viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack", "viewer_unstack": "Un-Stack",
"scaffold_body_error_occurred": "Error occurred", "scaffold_body_error_occured": "Error occured"
"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"
} }

View file

@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.89.0" version_number: "1.88.2"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, 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 { Future<void> fetchStatistics() async {
final count = state = await _activityService.getStatistics(albumId, assetId: assetId);
await _activityService.getStatistics(albumId, assetId: assetId);
if (mounted) {
state = count;
}
} }
Future<void> addActivity() async { Future<void> addActivity() async {

View file

@ -68,46 +68,46 @@ class AlbumThumbnailListTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.circular(8),
child: album.thumbnail.value == null child: album.thumbnail.value == null
? buildEmptyThumbnail() ? buildEmptyThumbnail()
: buildAlbumThumbnail(), : buildAlbumThumbnail(),
), ),
Expanded( Padding(
child: Padding( padding: const EdgeInsets.only(
padding: const EdgeInsets.symmetric(horizontal: 8.0), left: 8.0,
child: Column( right: 8.0,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
album.name, children: [
overflow: TextOverflow.ellipsis, Text(
style: const TextStyle( album.name,
fontWeight: FontWeight.bold, style: const TextStyle(
), fontWeight: FontWeight.bold,
), ),
Row( ),
mainAxisSize: MainAxisSize.min, Row(
children: [ mainAxisSize: MainAxisSize.min,
Text( children: [
album.assetCount == 1 Text(
? 'album_thumbnail_card_item' album.assetCount == 1
: 'album_thumbnail_card_items', ? 'album_thumbnail_card_item'
style: const TextStyle( : 'album_thumbnail_card_items',
style: const TextStyle(
fontSize: 12,
),
).tr(args: ['${album.assetCount}']),
if (album.shared)
const Text(
'album_thumbnail_card_shared',
style: TextStyle(
fontSize: 12, fontSize: 12,
), ),
).tr(args: ['${album.assetCount}']), ).tr(),
if (album.shared) ],
const Text( ),
'album_thumbnail_card_shared', ],
style: TextStyle(
fontSize: 12,
),
).tr(),
],
),
],
),
), ),
), ),
], ],

View file

@ -135,56 +135,49 @@ class LibraryPage extends HookConsumerWidget {
} }
Widget buildCreateAlbumButton() { Widget buildCreateAlbumButton() {
return LayoutBuilder( return GestureDetector(
builder: (context, constraints) { onTap: () {
var cardSize = constraints.maxWidth; context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
return GestureDetector(
onTap: () {
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
},
child: Padding(
padding:
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: cardSize,
height: cardSize,
decoration: BoxDecoration(
border: Border.all(
color: isDarkTheme
? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203),
),
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: context.primaryColor,
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 8.0,
bottom: 16,
),
child: Text(
'library_page_new_album',
style: context.textTheme.labelLarge,
).tr(),
),
],
),
),
);
}, },
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: isDarkTheme
? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203),
),
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: context.primaryColor,
),
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 8.0,
bottom: 16,
),
child: Text(
'library_page_new_album',
style: context.textTheme.labelLarge,
).tr(),
),
],
),
),
); );
} }

View file

@ -4,15 +4,14 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_viewer/ui/description_input.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.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/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.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:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -22,84 +21,111 @@ class ExifBottomSheet extends HookConsumerWidget {
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key); const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
bool hasCoordinates(ExifInfo? exifInfo) =>
exifInfo != null &&
exifInfo.latitude != null &&
exifInfo.longitude != null &&
exifInfo.latitude != 0 &&
exifInfo.longitude != 0;
String formatTimeZone(Duration d) =>
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
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);
}
}
}
}
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;
}
final double latitude = exifInfo!.latitude!;
final double longitude = exifInfo.longitude!;
const zoomLevel = 16;
if (Platform.isAndroid) {
Uri uri = Uri(
scheme: 'geo',
host: '$latitude,$longitude',
queryParameters: {
'z': '$zoomLevel',
'q': '$latitude,$longitude($formattedDateTime)',
},
);
if (await canLaunchUrl(uri)) {
return uri;
}
} else if (Platform.isIOS) {
var params = {
'll': '$latitude,$longitude',
'q': formattedDateTime,
'z': '$zoomLevel',
};
Uri uri = Uri.https('maps.apple.com', '/', params);
if (await canLaunchUrl(uri)) {
return uri;
}
}
return Uri(
scheme: 'https',
host: 'openstreetmap.org',
queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'},
fragment: 'map=$zoomLevel/$latitude/$longitude',
);
}
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset)); final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo; final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var textColor = context.isDarkTheme ? Colors.white : Colors.black; var textColor = context.isDarkTheme ? Colors.white : Colors.black;
bool hasCoordinates() =>
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);
return '$date$time GMT${timeZone.formatAsOffset()}';
}
Future<Uri?> createCoordinatesUri() async {
if (!hasCoordinates()) {
return null;
}
final double latitude = exifInfo!.latitude!;
final double longitude = exifInfo.longitude!;
const zoomLevel = 16;
if (Platform.isAndroid) {
Uri uri = Uri(
scheme: 'geo',
host: '$latitude,$longitude',
queryParameters: {
'z': '$zoomLevel',
'q': '$latitude,$longitude($formattedDateTime)',
},
);
if (await canLaunchUrl(uri)) {
return uri;
}
} else if (Platform.isIOS) {
var params = {
'll': '$latitude,$longitude',
'q': formattedDateTime,
'z': '$zoomLevel',
};
Uri uri = Uri.https('maps.apple.com', '/', params);
if (await canLaunchUrl(uri)) {
return uri;
}
}
return Uri(
scheme: 'https',
host: 'openstreetmap.org',
queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'},
fragment: 'map=$zoomLevel/$latitude/$longitude',
);
}
buildMap() { buildMap() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return MapThumbnail( return MapThumbnail(
showAttribution: false,
coords: LatLng( coords: LatLng(
exifInfo?.latitude ?? 0, exifInfo?.latitude ?? 0,
exifInfo?.longitude ?? 0, exifInfo?.longitude ?? 0,
), ),
height: 150, height: 150,
width: constraints.maxWidth, zoom: 16.0,
zoom: 12.0,
markers: [ markers: [
Marker( Marker(
anchorPos: AnchorPos.align(AnchorAlign.top), anchorPos: AnchorPos.align(AnchorAlign.top),
@ -113,7 +139,7 @@ class ExifBottomSheet extends HookConsumerWidget {
), ),
], ],
onTap: (tapPosition, latLong) async { onTap: (tapPosition, latLong) async {
Uri? uri = await createCoordinatesUri(); Uri? uri = await _createCoordinatesUri(exifInfo);
if (uri == null) { if (uri == null) {
return; return;
@ -155,26 +181,8 @@ class ExifBottomSheet extends HookConsumerWidget {
buildLocation() { buildLocation() {
// Guard no lat/lng // Guard no lat/lng
if (!hasCoordinates()) { if (!hasCoordinates(exifInfo)) {
return asset.isRemote return Container();
? 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();
} }
return Column( return Column(
@ -183,29 +191,13 @@ class ExifBottomSheet extends HookConsumerWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, "exif_bottom_sheet_location",
children: [ style: context.textTheme.labelMedium?.copyWith(
Text( color: context.textTheme.labelMedium?.color?.withAlpha(200),
"exif_bottom_sheet_location", fontWeight: FontWeight.w600,
style: context.textTheme.labelMedium?.copyWith( ),
color: ).tr(),
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(), buildMap(),
RichText( RichText(
text: TextSpan( text: TextSpan(
@ -241,27 +233,12 @@ class ExifBottomSheet extends HookConsumerWidget {
} }
buildDate() { buildDate() {
return Row( return Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, formattedDateTime,
children: [ style: const TextStyle(
Text( fontWeight: FontWeight.bold,
formattedDateTime(), fontSize: 14,
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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Flexible( Flexible(
flex: hasCoordinates() ? 5 : 0, flex: hasCoordinates(exifInfo) ? 5 : 0,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: buildLocation(), child: buildLocation(),
@ -425,8 +402,9 @@ class ExifBottomSheet extends HookConsumerWidget {
child: CircularProgressIndicator.adaptive(), child: CircularProgressIndicator.adaptive(),
), ),
), ),
const SizedBox(height: 8.0),
buildLocation(), buildLocation(),
SizedBox(height: hasCoordinates() ? 16.0 : 6.0), SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),
buildDetail(), buildDetail(),
const SizedBox(height: 50), const SizedBox(height: 50),
], ],

View file

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

View file

@ -375,8 +375,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
await _getBackupAlbumsInfo(); await _getBackupAlbumsInfo();
await updateServerInfo(); await updateServerInfo();
await _updateBackupAssetCount(); 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() onCreateNewAlbum;
final void Function() onUpload; final void Function() onUpload;
final void Function() onStack; final void Function() onStack;
final void Function() onEditTime;
final void Function() onEditLocation;
final List<Album> albums; final List<Album> albums;
final List<Album> sharedAlbums; final List<Album> sharedAlbums;
@ -39,8 +37,6 @@ class ControlBottomAppBar extends ConsumerWidget {
required this.onCreateNewAlbum, required this.onCreateNewAlbum,
required this.onUpload, required this.onUpload,
required this.onStack, required this.onStack,
required this.onEditTime,
required this.onEditLocation,
this.selectionAssetState = const SelectionAssetState(), this.selectionAssetState = const SelectionAssetState(),
this.enabled = true, this.enabled = true,
}) : super(key: key); }) : super(key: key);
@ -78,18 +74,6 @@ class ControlBottomAppBar extends ConsumerWidget {
label: "control_bottom_app_bar_favorite".tr(), label: "control_bottom_app_bar_favorite".tr(),
onPressed: enabled ? onFavorite : null, 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( ControlBoxButton(
iconData: Icons.delete_outline_rounded, iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(), label: "control_bottom_app_bar_delete".tr(),

View file

@ -213,10 +213,10 @@ class HomePage extends HookConsumerWidget {
processing.value = true; processing.value = true;
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
try { try {
ref.read(manualUploadProvider.notifier).uploadAssets( ref.read(manualUploadProvider.notifier).uploadAssets(
context, context,
selection.value.where((a) => a.storage == AssetState.local), selection.value.where((a) => a.storage == AssetState.local),
); );
} finally { } finally {
processing.value = false; processing.value = false;
} }
@ -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 { Future<void> refreshAssets() async {
final fullRefresh = refreshCount.value > 0; final fullRefresh = refreshCount.value > 0;
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh); await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
@ -439,8 +411,6 @@ class HomePage extends HookConsumerWidget {
enabled: !processing.value, enabled: !processing.value,
selectionAssetState: selectionAssetState.value, selectionAssetState: selectionAssetState.value,
onStack: onStack, 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/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/shared/models/store.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
@ -23,7 +21,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier( AuthenticationNotifier(
this._apiService, this._apiService,
this._db, this._db,
this._ref,
) : super( ) : super(
AuthenticationState( AuthenticationState(
deviceId: "", deviceId: "",
@ -39,8 +36,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
final ApiService _apiService; final ApiService _apiService;
final Isar _db; final Isar _db;
final StateNotifierProviderRef<AuthenticationNotifier, AuthenticationState>
_ref;
final _log = Logger("AuthenticationNotifier"); final _log = Logger("AuthenticationNotifier");
Future<bool> login( Future<bool> login(
@ -116,8 +111,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.delete(StoreKey.currentUser), Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken), Store.delete(StoreKey.accessToken),
]); ]);
_ref.invalidate(albumProvider);
_ref.invalidate(sharedAlbumProvider);
state = state.copyWith( state = state.copyWith(
deviceId: "", deviceId: "",
@ -229,6 +222,5 @@ final authenticationProvider =
return AuthenticationNotifier( return AuthenticationNotifier(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(dbProvider), 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/plugin_api.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/map_state.provider.dart';
import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -14,15 +12,13 @@ class MapThumbnail extends HookConsumerWidget {
final double zoom; final double zoom;
final List<Marker> markers; final List<Marker> markers;
final double height; final double height;
final double width;
final bool showAttribution; final bool showAttribution;
final bool isDarkTheme; final bool isDarkTheme;
const MapThumbnail({ const MapThumbnail({
super.key, super.key,
required this.coords, required this.coords,
this.height = 100, required this.height,
this.width = 100,
this.onTap, this.onTap,
this.zoom = 1, this.zoom = 1,
this.showAttribution = true, this.showAttribution = true,
@ -32,33 +28,18 @@ class MapThumbnail extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final mapController = useMapController();
final isMapReady = useRef(false);
ref.watch(mapStateNotifier.select((s) => s.mapStyle)); ref.watch(mapStateNotifier.select((s) => s.mapStyle));
useEffect(
() {
if (isMapReady.value && mapController.center != coords) {
mapController.move(coords, zoom);
}
return null;
},
[coords],
);
return SizedBox( return SizedBox(
height: height, height: height,
width: width,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)), borderRadius: const BorderRadius.all(Radius.circular(15)),
child: FlutterMap( child: FlutterMap(
mapController: mapController,
options: MapOptions( options: MapOptions(
interactiveFlags: InteractiveFlag.none, interactiveFlags: InteractiveFlag.none,
center: coords, center: coords,
zoom: zoom, zoom: zoom,
onTap: onTap, onTap: onTap,
onMapReady: () => isMapReady.value = true,
), ),
nonRotatedChildren: [ nonRotatedChildren: [
if (showAttribution) 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 // in onMapEvent() since MapEventMove#id is not populated properly in the
// current version of flutter_map(4.0.0) used // current version of flutter_map(4.0.0) used
bool forceAssetUpdate = false; bool forceAssetUpdate = false;
bool isMapReady = false;
late final Debounce debounce; late final Debounce debounce;
@override @override
@ -80,7 +79,7 @@ class MapPageState extends ConsumerState<MapPage> {
bool forceReload = false, bool forceReload = false,
}) { }) {
try { try {
final bounds = isMapReady ? mapController.bounds : null; final bounds = mapController.bounds;
if (bounds != null) { if (bounds != null) {
final oldAssetsInBounds = assetsInBounds.toSet(); final oldAssetsInBounds = assetsInBounds.toSet();
assetsInBounds = assetsInBounds =
@ -456,7 +455,6 @@ class MapPageState extends ConsumerState<MapPage> {
minZoom: 1, minZoom: 1,
maxZoom: maxZoom, maxZoom: maxZoom,
onMapReady: () { onMapReady: () {
isMapReady = true;
mapController.mapEventStream.listen(onMapEvent); mapController.mapEventStream.listen(onMapEvent);
}, },
), ),

View file

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

View file

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

View file

@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
// ************************************************************************** // **************************************************************************
String _$appSettingsServiceHash() => String _$appSettingsServiceHash() =>
r'45ea609a91d250290431a7a08a14d16b37c7515d'; r'957a65af6967701112f3076b507f9738fec4b7be';
/// See also [appSettingsService]. /// See also [appSettingsService].
@ProviderFor(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/asset_selection_page.dart';
import 'package:immich_mobile/modules/album/views/create_album_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/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/map/views/map_page.dart';
import 'package:immich_mobile/modules/memories/models/memory.dart'; import 'package:immich_mobile/modules/memories/models/memory.dart';
import 'package:immich_mobile/modules/memories/views/memory_page.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/search/views/search_result_page.dart';
import 'package:immich_mobile/modules/settings/views/settings_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/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/duplicate_guard.dart';
import 'package:immich_mobile/routing/backup_permission_guard.dart'; import 'package:immich_mobile/routing/backup_permission_guard.dart';
import 'package:immich_mobile/shared/models/asset.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/splash_screen.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart'; import 'package:immich_mobile/shared/views/tab_controller_page.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:photo_manager/photo_manager.dart' hide LatLng; import 'package:photo_manager/photo_manager.dart';
import 'package:latlong2/latlong.dart';
part 'router.gr.dart'; part 'router.gr.dart';
@ -89,10 +86,9 @@ part 'router.gr.dart';
], ],
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
CustomRoute( AutoRoute(
page: GalleryViewerPage, page: GalleryViewerPage,
guards: [AuthGuard, DuplicateGuard], guards: [AuthGuard, DuplicateGuard],
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
), ),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
AutoRoute( AutoRoute(
@ -174,10 +170,6 @@ part 'router.gr.dart';
transitionsBuilder: TransitionsBuilders.slideLeft, transitionsBuilder: TransitionsBuilders.slideLeft,
durationInMilliseconds: 200, durationInMilliseconds: 200,
), ),
CustomRoute<LatLng?>(
page: MapLocationPickerPage,
guards: [AuthGuard, DuplicateGuard],
),
], ],
) )
class AppRouter extends _$AppRouter { class AppRouter extends _$AppRouter {

View file

@ -63,7 +63,7 @@ class _$AppRouter extends RootStackRouter {
}, },
GalleryViewerRoute.name: (routeData) { GalleryViewerRoute.name: (routeData) {
final args = routeData.argsAs<GalleryViewerRouteArgs>(); final args = routeData.argsAs<GalleryViewerRouteArgs>();
return CustomPage<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
child: GalleryViewerPage( child: GalleryViewerPage(
key: args.key, key: args.key,
@ -75,9 +75,6 @@ class _$AppRouter extends RootStackRouter {
isOwner: args.isOwner, isOwner: args.isOwner,
sharedAlbumId: args.sharedAlbumId, sharedAlbumId: args.sharedAlbumId,
), ),
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
opaque: true,
barrierDismissible: false,
); );
}, },
VideoViewerRoute.name: (routeData) { VideoViewerRoute.name: (routeData) {
@ -360,19 +357,6 @@ class _$AppRouter extends RootStackRouter {
barrierDismissible: false, 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) { HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
@ -717,14 +701,6 @@ class _$AppRouter extends RootStackRouter {
duplicateGuard, 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 /// generated route for
/// [HomePage] /// [HomePage]
class HomeRoute extends PageRouteInfo<void> { class HomeRoute extends PageRouteInfo<void> {

View file

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

View file

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

View file

@ -1,13 +1,10 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/asset.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/server_info/server_version.model.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/services/sync.service.dart'; import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
@ -17,33 +14,13 @@ import 'package:socket_io_client/socket_io_client.dart';
enum PendingAction { enum PendingAction {
assetDelete, assetDelete,
assetUploaded,
assetHidden,
} }
class PendingChange { class PendingChange {
final String id;
final PendingAction action; final PendingAction action;
final dynamic value; final dynamic value;
const PendingChange( const PendingChange(this.action, this.value);
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;
} }
class WebsocketState { class WebsocketState {
@ -154,7 +131,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_asset_trash', _handleServerUpdates); socket.on('on_asset_trash', _handleServerUpdates);
socket.on('on_asset_restore', _handleServerUpdates); socket.on('on_asset_restore', _handleServerUpdates);
socket.on('on_asset_update', _handleServerUpdates); socket.on('on_asset_update', _handleServerUpdates);
socket.on('on_asset_hidden', _handleOnAssetHidden);
socket.on('on_new_release', _handleReleaseUpdates); socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) { } catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
@ -187,78 +163,35 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
} }
void addPendingChange(PendingAction action, dynamic value) { void addPendingChange(PendingAction action, dynamic value) {
final now = DateTime.now();
state = state.copyWith( state = state.copyWith(
pendingChanges: [ pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
...state.pendingChanges,
PendingChange(now.millisecondsSinceEpoch.toString(), action, value),
],
); );
_debounce(handlePendingChanges);
} }
Future<void> _handlePendingDeletes() async { void handlePendingChanges() {
final deleteChanges = state.pendingChanges final deleteChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetDelete) .where((c) => c.action == PendingAction.assetDelete)
.toList(); .toList();
if (deleteChanges.isNotEmpty) { if (deleteChanges.isNotEmpty) {
List<String> remoteIds = List<String> remoteIds =
deleteChanges.map((a) => a.value.toString()).toList(); deleteChanges.map((a) => a.value.toString()).toList();
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
state = state.copyWith( state = state.copyWith(
pendingChanges: state.pendingChanges pendingChanges: state.pendingChanges
.whereNot((c) => deleteChanges.contains(c)) .where((c) => c.action != PendingAction.assetDelete)
.toList(), .toList(),
); );
} }
} }
Future<void> _handlePendingUploaded() async { void _handleOnUploadSuccess(dynamic data) {
final uploadedChanges = state.pendingChanges final dto = AssetResponseDto.fromJson(data);
.where((c) => c.action == PendingAction.assetUploaded) if (dto != null) {
.toList(); final newAsset = Asset.remote(dto);
if (uploadedChanges.isNotEmpty) { _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
List<AssetResponseDto?> remoteAssets = uploadedChanges
.map((a) => AssetResponseDto.fromJson(a.value))
.toList();
for (final dto in remoteAssets) {
if (dto != null) {
final newAsset = Asset.remote(dto);
await _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 _) { void _handleOnConfigUpdate(dynamic _) {
_ref.read(serverInfoProvider.notifier).getServerFeatures(); _ref.read(serverInfoProvider.notifier).getServerFeatures();
_ref.read(serverInfoProvider.notifier).getServerConfig(); _ref.read(serverInfoProvider.notifier).getServerConfig();
@ -269,14 +202,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_ref.read(assetProvider.notifier).getAllAsset(); _ref.read(assetProvider.notifier).getAllAsset();
} }
void _handleOnUploadSuccess(dynamic data) => void _handleOnAssetDelete(dynamic data) {
addPendingChange(PendingAction.assetUploaded, data); addPendingChange(PendingAction.assetDelete, data);
_debounce(handlePendingChanges);
void _handleOnAssetDelete(dynamic data) => }
addPendingChange(PendingAction.assetDelete, data);
void _handleOnAssetHidden(dynamic data) =>
addPendingChange(PendingAction.assetHidden, data);
_handleReleaseUpdates(dynamic data) { _handleReleaseUpdates(dynamic data) {
// Json guard // 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/api.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart'; import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:latlong2/latlong.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -182,27 +181,4 @@ class AssetService {
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) { Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
return updateAssets(assets, UpdateAssetDto(isArchived: 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

@ -401,10 +401,6 @@ class SyncService {
final Album a = await Album.remote(dto); final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a)); 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, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"scaffold_body_error_occurred", "scaffold_body_error_occured",
style: context.textTheme.displayMedium, style: context.textTheme.displayMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),

View file

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

View file

@ -2,17 +2,12 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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/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/immich_toast.dart';
import 'package:immich_mobile/shared/ui/location_picker.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart'; import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:latlong2/latlong.dart';
void handleShareAssets( void handleShareAssets(
WidgetRef ref, 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/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md doc/AssetBulkUploadCheckResult.md
doc/AssetFaceResponseDto.md
doc/AssetFaceUpdateDto.md
doc/AssetFaceUpdateItem.md
doc/AssetFaceWithoutPersonResponseDto.md
doc/AssetFileUploadResponseDto.md doc/AssetFileUploadResponseDto.md
doc/AssetIdsDto.md doc/AssetIdsDto.md
doc/AssetIdsResponseDto.md doc/AssetIdsResponseDto.md
@ -64,8 +60,6 @@ doc/DownloadInfoDto.md
doc/DownloadResponseDto.md doc/DownloadResponseDto.md
doc/EntityType.md doc/EntityType.md
doc/ExifResponseDto.md doc/ExifResponseDto.md
doc/FaceApi.md
doc/FaceDto.md
doc/FileChecksumDto.md doc/FileChecksumDto.md
doc/FileChecksumResponseDto.md doc/FileChecksumResponseDto.md
doc/FileReportDto.md doc/FileReportDto.md
@ -106,7 +100,6 @@ doc/PersonApi.md
doc/PersonResponseDto.md doc/PersonResponseDto.md
doc/PersonStatisticsResponseDto.md doc/PersonStatisticsResponseDto.md
doc/PersonUpdateDto.md doc/PersonUpdateDto.md
doc/PersonWithFacesResponseDto.md
doc/QueueStatusDto.md doc/QueueStatusDto.md
doc/ReactionLevel.md doc/ReactionLevel.md
doc/ReactionType.md doc/ReactionType.md
@ -184,7 +177,6 @@ lib/api/api_key_api.dart
lib/api/asset_api.dart lib/api/asset_api.dart
lib/api/audit_api.dart lib/api/audit_api.dart
lib/api/authentication_api.dart lib/api/authentication_api.dart
lib/api/face_api.dart
lib/api/job_api.dart lib/api/job_api.dart
lib/api/library_api.dart lib/api/library_api.dart
lib/api/o_auth_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_item.dart
lib/model/asset_bulk_upload_check_response_dto.dart lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.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_file_upload_response_dto.dart
lib/model/asset_ids_dto.dart lib/model/asset_ids_dto.dart
lib/model/asset_ids_response_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/download_response_dto.dart
lib/model/entity_type.dart lib/model/entity_type.dart
lib/model/exif_response_dto.dart lib/model/exif_response_dto.dart
lib/model/face_dto.dart
lib/model/file_checksum_dto.dart lib/model/file_checksum_dto.dart
lib/model/file_checksum_response_dto.dart lib/model/file_checksum_response_dto.dart
lib/model/file_report_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_response_dto.dart
lib/model/person_statistics_response_dto.dart lib/model/person_statistics_response_dto.dart
lib/model/person_update_dto.dart lib/model/person_update_dto.dart
lib/model/person_with_faces_response_dto.dart
lib/model/queue_status_dto.dart lib/model/queue_status_dto.dart
lib/model/reaction_level.dart lib/model/reaction_level.dart
lib/model/reaction_type.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_item_test.dart
test/asset_bulk_upload_check_response_dto_test.dart test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_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_file_upload_response_dto_test.dart
test/asset_ids_dto_test.dart test/asset_ids_dto_test.dart
test/asset_ids_response_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/download_response_dto_test.dart
test/entity_type_test.dart test/entity_type_test.dart
test/exif_response_dto_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_dto_test.dart
test/file_checksum_response_dto_test.dart test/file_checksum_response_dto_test.dart
test/file_report_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_response_dto_test.dart
test/person_statistics_response_dto_test.dart test/person_statistics_response_dto_test.dart
test/person_update_dto_test.dart test/person_update_dto_test.dart
test/person_with_faces_response_dto_test.dart
test/queue_status_dto_test.dart test/queue_status_dto_test.dart
test/reaction_level_test.dart test/reaction_level_test.dart
test/reaction_type_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: 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 - Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements ## Requirements
@ -133,8 +133,6 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices | *AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *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* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library | *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* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner |
*PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} | *PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
*PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /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* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person |
*PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} | *PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} |
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | *PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
*PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics | *PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | *PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge | *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* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
@ -228,10 +224,6 @@ Class | Method | HTTP request | Description
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md) - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.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) - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsDto](doc//AssetIdsDto.md)
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
@ -266,7 +258,6 @@ Class | Method | HTTP request | Description
- [DownloadResponseDto](doc//DownloadResponseDto.md) - [DownloadResponseDto](doc//DownloadResponseDto.md)
- [EntityType](doc//EntityType.md) - [EntityType](doc//EntityType.md)
- [ExifResponseDto](doc//ExifResponseDto.md) - [ExifResponseDto](doc//ExifResponseDto.md)
- [FaceDto](doc//FaceDto.md)
- [FileChecksumDto](doc//FileChecksumDto.md) - [FileChecksumDto](doc//FileChecksumDto.md)
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md) - [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
- [FileReportDto](doc//FileReportDto.md) - [FileReportDto](doc//FileReportDto.md)
@ -302,7 +293,6 @@ Class | Method | HTTP request | Description
- [PersonResponseDto](doc//PersonResponseDto.md) - [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md)
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md) - [QueueStatusDto](doc//QueueStatusDto.md)
- [ReactionLevel](doc//ReactionLevel.md) - [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md) - [ReactionType](doc//ReactionType.md)

View file

@ -8,12 +8,9 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**ids** | **List<String>** | | [default to const []] **ids** | **List<String>** | | [default to const []]
**isArchived** | **bool** | | [optional] **isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional] **isFavorite** | **bool** | | [optional]
**latitude** | **num** | | [optional]
**longitude** | **num** | | [optional]
**removeParent** | **bool** | | [optional] **removeParent** | **bool** | | [optional]
**stackParentId** | **String** | | [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** | | **originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional] **owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
**ownerId** | **String** | | **ownerId** | **String** | |
**people** | [**List<PersonWithFacesResponseDto>**](PersonWithFacesResponseDto.md) | | [optional] [default to const []] **people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | | **resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional] **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []] **stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]

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 Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**createPerson**](PersonApi.md#createperson) | **POST** /person |
[**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person | [**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person |
[**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} | [**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} |
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | [**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
[**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics | [**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | [**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge | [**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
[**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person | [**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} | [**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** # **getAllPeople**
> PeopleResponseDto getAllPeople(withHidden) > 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) [[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** # **updatePeople**
> List<BulkIdResponseDto> updatePeople(peopleUpdateDto) > 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 ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**description** | **String** | | [optional] **description** | **String** | | [optional]
**isArchived** | **bool** | | [optional] **isArchived** | **bool** | | [optional]
**isFavorite** | **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) [[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/asset_api.dart';
part 'api/audit_api.dart'; part 'api/audit_api.dart';
part 'api/authentication_api.dart'; part 'api/authentication_api.dart';
part 'api/face_api.dart';
part 'api/job_api.dart'; part 'api/job_api.dart';
part 'api/library_api.dart'; part 'api/library_api.dart';
part 'api/o_auth_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_item.dart';
part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_response_dto.dart';
part 'model/asset_bulk_upload_check_result.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_file_upload_response_dto.dart';
part 'model/asset_ids_dto.dart'; part 'model/asset_ids_dto.dart';
part 'model/asset_ids_response_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/download_response_dto.dart';
part 'model/entity_type.dart'; part 'model/entity_type.dart';
part 'model/exif_response_dto.dart'; part 'model/exif_response_dto.dart';
part 'model/face_dto.dart';
part 'model/file_checksum_dto.dart'; part 'model/file_checksum_dto.dart';
part 'model/file_checksum_response_dto.dart'; part 'model/file_checksum_response_dto.dart';
part 'model/file_report_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_response_dto.dart';
part 'model/person_statistics_response_dto.dart'; part 'model/person_statistics_response_dto.dart';
part 'model/person_update_dto.dart'; part 'model/person_update_dto.dart';
part 'model/person_with_faces_response_dto.dart';
part 'model/queue_status_dto.dart'; part 'model/queue_status_dto.dart';
part 'model/reaction_level.dart'; part 'model/reaction_level.dart';
part 'model/reaction_type.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; 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]. /// Performs an HTTP 'GET /person' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
@ -358,61 +317,6 @@ class PersonApi {
return null; 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]. /// Performs an HTTP 'PUT /person' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View file

@ -215,14 +215,6 @@ class ApiClient {
return AssetBulkUploadCheckResponseDto.fromJson(value); return AssetBulkUploadCheckResponseDto.fromJson(value);
case 'AssetBulkUploadCheckResult': case 'AssetBulkUploadCheckResult':
return AssetBulkUploadCheckResult.fromJson(value); 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': case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value); return AssetFileUploadResponseDto.fromJson(value);
case 'AssetIdsDto': case 'AssetIdsDto':
@ -291,8 +283,6 @@ class ApiClient {
return EntityTypeTypeTransformer().decode(value); return EntityTypeTypeTransformer().decode(value);
case 'ExifResponseDto': case 'ExifResponseDto':
return ExifResponseDto.fromJson(value); return ExifResponseDto.fromJson(value);
case 'FaceDto':
return FaceDto.fromJson(value);
case 'FileChecksumDto': case 'FileChecksumDto':
return FileChecksumDto.fromJson(value); return FileChecksumDto.fromJson(value);
case 'FileChecksumResponseDto': case 'FileChecksumResponseDto':
@ -363,8 +353,6 @@ class ApiClient {
return PersonStatisticsResponseDto.fromJson(value); return PersonStatisticsResponseDto.fromJson(value);
case 'PersonUpdateDto': case 'PersonUpdateDto':
return PersonUpdateDto.fromJson(value); return PersonUpdateDto.fromJson(value);
case 'PersonWithFacesResponseDto':
return PersonWithFacesResponseDto.fromJson(value);
case 'QueueStatusDto': case 'QueueStatusDto':
return QueueStatusDto.fromJson(value); return QueueStatusDto.fromJson(value);
case 'ReactionLevel': case 'ReactionLevel':

View file

@ -13,24 +13,13 @@ part of openapi.api;
class AssetBulkUpdateDto { class AssetBulkUpdateDto {
/// Returns a new [AssetBulkUpdateDto] instance. /// Returns a new [AssetBulkUpdateDto] instance.
AssetBulkUpdateDto({ AssetBulkUpdateDto({
this.dateTimeOriginal,
this.ids = const [], this.ids = const [],
this.isArchived, this.isArchived,
this.isFavorite, this.isFavorite,
this.latitude,
this.longitude,
this.removeParent, this.removeParent,
this.stackParentId, 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; List<String> ids;
/// ///
@ -49,22 +38,6 @@ class AssetBulkUpdateDto {
/// ///
bool? isFavorite; 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 /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -83,37 +56,26 @@ class AssetBulkUpdateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.ids == ids && other.ids == ids &&
other.isArchived == isArchived && other.isArchived == isArchived &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
other.removeParent == removeParent && other.removeParent == removeParent &&
other.stackParentId == stackParentId; other.stackParentId == stackParentId;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(ids.hashCode) + (ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) + (isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(removeParent == null ? 0 : removeParent!.hashCode) + (removeParent == null ? 0 : removeParent!.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode); (stackParentId == null ? 0 : stackParentId!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
} else {
// json[r'dateTimeOriginal'] = null;
}
json[r'ids'] = this.ids; json[r'ids'] = this.ids;
if (this.isArchived != null) { if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived; json[r'isArchived'] = this.isArchived;
@ -125,16 +87,6 @@ class AssetBulkUpdateDto {
} else { } else {
// json[r'isFavorite'] = null; // 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) { if (this.removeParent != null) {
json[r'removeParent'] = this.removeParent; json[r'removeParent'] = this.removeParent;
} else { } else {
@ -156,18 +108,11 @@ class AssetBulkUpdateDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AssetBulkUpdateDto( return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
ids: json[r'ids'] is List ids: json[r'ids'] is List
? (json[r'ids'] as List).cast<String>() ? (json[r'ids'] as List).cast<String>()
: const [], : const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'), isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), 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'), removeParent: mapValueOfType<bool>(json, r'removeParent'),
stackParentId: mapValueOfType<String>(json, r'stackParentId'), 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; String ownerId;
List<PersonWithFacesResponseDto> people; List<PersonResponseDto> people;
bool resized; bool resized;
@ -299,7 +299,7 @@ class AssetResponseDto {
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']), owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonWithFacesResponseDto.listFromJson(json[r'people']), people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!, resized: mapValueOfType<bool>(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetResponseDto.listFromJson(json[r'stack']), stack: AssetResponseDto.listFromJson(json[r'stack']),

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

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