Compare commits

...

101 Commits

Author SHA1 Message Date
Alex Tran
736b968eab merge main 2024-04-22 06:52:30 -05:00
Alex Tran
d5d8426bda Merge branch 'main' of github.com:immich-app/immich into web/automation-ui 2024-04-22 06:51:57 -05:00
Alex
be4a783845 fix(web): wrong month on timeline scrollbar cursor (#8996)
* fix(web): wrong month on timeline scrollbar cursor

* revert unnesessary change
2024-04-22 06:22:59 -05:00
Mert
c30cd3b378 chore: test more formats in e2e (#9001) 2024-04-22 01:35:27 -04:00
TruongSinh Tran-Nguyen
0d3cc28f45 feat(web): support 360 video (equirectangular) (#8762)
* [web]: support 360 video

* lint

* lint

* fix typing

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-21 19:14:54 +00:00
Conner Hnatiuk
f004487be0 fix(web): trash page now auto refreshes (#8978)
* fix(web): the trash page now auto refreshes when restore all or empty trash is clicked. Also shows number of assets affected.

* formatting
2024-04-21 14:07:17 -05:00
clementdelestre
21231d53a5 feat(mobile): add i18n in multiselect-grid and update translation (en and fr) (#8993)
* add i18n in multiselect grid (en-fr)

* add FR translations from (haptic feedback)

* revert settings
2024-04-21 13:26:19 -05:00
Daniel Dietzler
a99862120d feat: mobile label for renovate pull requests (#8991)
mobile lable for renovate pull requests
2024-04-21 13:11:03 -05:00
shenlong
776023b149 dep(mobile): upgrade gradle (#8409)
* dep(mobile): upgrade gradle

* chore(deps): update kotlin & guava

* build: change java version and flutter test version

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-04-20 23:07:32 -05:00
martin
7d4187962a feat(web): new look option for slideshow (#8924)
feat: new look option for slideshow
2024-04-20 23:06:49 -05:00
Jason Rasmussen
a93534fc3c refactor(server): session interface types (#8977) 2024-04-20 23:45:55 -04:00
Alex
cef84f6ced chore(mobile): override appbundle on PlayStore before getting released (#8960) 2024-04-20 19:56:03 -05:00
Alex The Bot
a2180a467d Version v1.102.3 2024-04-20 20:17:39 +00:00
Jason Rasmussen
1e3dceea4d fix(server): session refresh (#8974) 2024-04-20 15:15:25 -05:00
Mert
fd4514711f feat(server): enable AV1 encoding for NVENC (#8959)
allow av1 for nvenc
2024-04-20 14:52:50 -04:00
Alex
2dd7c13b88 Revert "feat(android) Check server is reachable before starting background backup (#8594)" (#8958)
This reverts commit 71b6d8b569.
2024-04-20 12:15:26 -05:00
Alex
40931b5668 chore: post release tasks 2024-04-20 11:15:41 -05:00
Alex The Bot
25549b87c9 Version v1.102.2 2024-04-20 15:55:32 +00:00
Alex
7ec62f12b5 Revert "fix(mobile): random logout (#8739)" (#8954)
This reverts commit 97c099e26d.
2024-04-20 10:53:52 -05:00
Jaryl Chng
caf76f0713 feat(server): enable AV1 encoding for QSV (#8942) 2024-04-20 10:36:00 -04:00
martin
6778653825 fix(web): keep focus when searching people (#8950)
fix: keep focus when searching people

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-20 14:18:31 +00:00
Alex
c858b43717 chore: post release tasks 2024-04-20 09:12:11 -05:00
Alex The Bot
6eb1b82541 Version v1.102.1 2024-04-20 13:43:46 +00:00
devjn
71b6d8b569 feat(android) Check server is reachable before starting background backup (#8594)
* Bump androidx work version to 2.9.0

* Check that server is reachable before starting backup work

* Dart format

* Cleanup debug logs

* Fix analysis
2024-04-20 08:39:04 -05:00
Conner
3abfe3c99e fix(web): restore button in asset viewer (#8935)
* fix(web): restore button added to trashed asset-view to restore single item

* fixed the asset-viewer menu to update upon restoration

* prettier formatting complete, testing passed

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-04-20 01:19:50 +00:00
Jason Rasmussen
171b6bb0a6 refactor: system metadata (#8923)
refactor(server): system metadata
2024-04-19 20:36:15 -04:00
Daniel Dietzler
78c7ff855d refactor(server): move file file report endpoints to their own controller (#8925)
* move file report to its own controller

* chore: open api
2024-04-19 20:35:54 -04:00
Alex
57be9182d4 chore: post release tasks 2024-04-19 15:32:45 -05:00
Alex The Bot
886e07604e Version v1.102.0 2024-04-19 20:08:02 +00:00
Mert
431ffebddd feat(server): use embedded preview from raw images (#8773)
* extract embedded

* update api

* add tests

* move temp file logic outside of media repo

* formatting

* revert `toSorted`

* disable by default

* clarify setting description

* wording

* wording

* update docs

* check extracted image dimensions

* test that it unlinks

* formatting

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-04-19 10:50:13 -05:00
Jason Rasmussen
74c921148b refactor(server): cookies (#8920) 2024-04-19 11:19:23 -04:00
martin
eaf9e5e477 feat(web): add an option to fill the screen with the slideshow view (#8909)
* feat: add an option to fill the screen with the slideshow view

* fix: rename var
2024-04-19 06:49:29 -04:00
Jason Rasmussen
4478e524f8 refactor(server): sessions (#8915)
* refactor: auth device => sessions

* chore: open api
2024-04-19 06:47:29 -04:00
renovate[bot]
e72e41a7aa chore(deps): update redis:6.2-alpine docker digest to 84882e8 (#8912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 12:46:59 +02:00
martin
efd8f0d648 fix(web): notification number of people when editing faces (#7352)
* fix: notification number of people when editing faces

* fix: lint

* fix: use id instead of index

* rename
2024-04-18 22:55:11 -04:00
renovate[bot]
d2b5cc6a4a chore(deps): update registry.hub.docker.com/library/redis:6.2-alpine docker digest to 84882e8 (#8913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-18 22:44:37 -04:00
Mert
596c35dc00 fix(server): skip invisible assets for thumbnail generation and ml (#8891)
* skip invisible assets for thumbnail generation and ml

* no need to update job status

* fix thumbhash check order

* linting
2024-04-19 01:37:55 +00:00
martin
112d6d60ec feat(web): add page up and page down shortcuts (#8910)
feat: add page up and page down shortcuts
2024-04-18 21:11:54 -04:00
Ben McCann
c50241369a docs: link to storage label docs from storage template docs (#8911)
* docs: link to storage label docs from storage template docs

* docusaurus sucks
2024-04-18 21:08:09 -04:00
martyfuhry
b74f8273c2 fix:(mobile): Updates old IMMICH text from the mobile settings modal (#8906)
* fix: Removes old IMMICH text from the mobile settings modal

Removed old Snowburst One font from the pubspec

Removes SnowburstOne.ttf file

* Uses immich text now
2024-04-18 14:11:00 -05:00
Mert
8573c84605 fix(server): include archived images in face detection (#8892) 2024-04-17 23:47:24 -04:00
Alessandro Vitali
a4f805e99b Update oauth.md (#8794)
Removed closing brackets from oauth redirect URIs.
2024-04-17 18:59:09 +00:00
renovate[bot]
7db07bbe61 fix(deps): update dependency gunicorn to v22 [security] (#8863) 2024-04-17 11:23:24 -04:00
Jason Rasmussen
3a9df6dae8 refactor(server): immich-admin list-users (#8862) 2024-04-17 12:27:04 +00:00
Ethan Margaillan
c227f9893e feat(web): un-stack from the photos page ; fix stack count (#8419)
* feat(web): un-stack from the photos page ; fix stack count

* move stuff outside of try-catch block

* small optim
2024-04-17 07:55:07 -04:00
renovate[bot]
a3feca2580 chore(deps): update node.js to ec0c413 (#8833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 07:53:00 -04:00
renovate[bot]
b21566c2fc chore(deps): update node.js to d328c7b (#8829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 07:52:29 -04:00
Ben
1071396a4a fix(web,a11y): remove autofocus from input fields (#8857)
* fix(web,a11y): remove autofocus from input field

The autofocus attribute can cause the keyboard to unexpectedly appear
for mobile users, and override any other focus management that the
application is doing programatically.

* fix: always include people filter
2024-04-17 11:15:37 +02:00
Alex
7e92ef9428 Merge branch 'main' of github.com:immich-app/immich into web/automation-ui 2024-04-17 11:13:14 +02:00
Matthew Momjian
f58886514d docs: fix vectors grant... again (#8860) 2024-04-17 01:21:08 -04:00
Kevin Huang
17dc12cf7d fix(server): storage usage calculation for motion photos (#8722)
* ignore non external assets in external libraries during syncUsage

* only update storage usage if asset is from internal libraries

* update storage usage on motion photo video asset creation

* updated metadata service tests

* added a test

* simplified syncUsage condition

* check for library type upload instead of not external

* fixed broken sql

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-04-17 03:04:59 +00:00
renovate[bot]
6d4d0f86cf chore(deps): update base-image to v20240416 (major) (#8660)
* chore(deps): update base-image to v20240416

* fix e2e

* rename variable

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2024-04-16 22:55:05 -04:00
Jason Rasmussen
14b1425e98 feat(server): logging interceptor (#8859) 2024-04-16 19:21:57 -04:00
Matthew Momjian
c70d9f9055 docs: bunch of small changes (#8854)
* Update postgres-standalone.md

* Update postgres-standalone.md

* Update postgres-standalone.md

* Update postgres-standalone.md

* Update environment-variables.md

* Update postgres-standalone.md

* Update docker-compose.mdx

* Update environment-variables.md

* Update FAQ.mdx

* Update kubernetes.md

* Update kubernetes.md

* Update system-settings.md

* Update libraries.md

* Update supported-formats.md
2024-04-16 18:58:19 -04:00
renovate[bot]
18fa6018c0 fix(deps): update typescript-projects (#8834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 17:45:14 -04:00
Jason Rasmussen
47fb9bd213 fix(server): correlationId (#8858) 2024-04-16 17:31:49 -04:00
AmAn Sharma
6e6deec40c feat: use ILoggerRepository (#8855)
* Migrate ImmichLogger over to injected ILoggerRepository

* chore: cleanup and tests

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-04-16 17:30:31 -04:00
Jason Rasmussen
877207a2e6 chore(server): delete swap file (#8856) 2024-04-16 22:13:03 +02:00
Hannes Palmquist
64cfd017b4 add community project PSImmich (#8851) 2024-04-16 18:01:56 +00:00
renovate[bot]
4c4ebf769f chore(deps): update dependency ruff to v0.3.6 (#8850) 2024-04-16 12:53:04 -04:00
Matthew Momjian
28d081338b docs: update community Guide/Projects, small PG query updates (#8844)
* Update community-projects.tsx

* Update community-guides.tsx

* Update community-projects.tsx

* Update database-queries.md

* Update database-queries.md

* Update community-projects.tsx
2024-04-16 12:39:03 -04:00
Jason Rasmussen
50c9bc0336 chore: migrate to vitest (#7156)
* chore: jest => vitest

* chore: replace jest-when
2024-04-16 10:44:45 -04:00
dependabot[bot]
ed2e4e5217 chore(deps): bump stumpylog/image-cleaner-action from 0.5.0 to 0.6.0 (#8841)
Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: stumpylog/image-cleaner-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 10:39:36 +00:00
Alex
1aa8707b8a chore(mobile): full width language change dropdown (#8806)
* chore(mobile): full width language change dropdown

* linting
2024-04-16 08:53:20 +02:00
Fynn Petersen-Frey
103cb60a57 feat(server): efficient full app sync (#8755)
* feat(server): efficient full app sync

* add SQL, fix test compile issues

* fix linter warning

* new sync controller+service, add tests

* enable new sync controller+service

* Update server/src/services/sync.service.ts

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-04-16 07:26:37 +02:00
aviv926
58e516c766 Docs: minor changes (#8814)
* minor

* add image

* PR feedback

* npm run format:fix of course 4_4

* Remove what is not relevant

* pr feedback

* PR feedback

* revert npm run format

* Update docs/docs/FAQ.mdx

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* Update FAQ.mdx

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
2024-04-16 07:26:12 +02:00
Ben
bcdec25843 feat(web,a11y): consolidate BaseModal into FullScreenModal (#8787)
* feat(web,a11y): FullScreenModal sticky buttons

* chore(web): combine BaseModal into FullScreenModal

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-16 07:06:15 +02:00
Ben McCann
28f591d01b chore(mobile): update TODO comment (#8826) 2024-04-16 07:05:50 +02:00
Jason Rasmussen
dba365634a chore(server): cleanup library watching (#8835)
chore: clean up library watching
2024-04-15 23:05:08 -04:00
renovate[bot]
1c1e461936 chore(deps): update mambaorg/micromamba:bookworm-slim docker digest to 4de6145 (#8828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 00:40:12 +00:00
Jason Rasmussen
2db76034b1 feat(server): correlation id via injected logger (#8823)
* feat(server): correlation id via injected logger

* feat: cid response header
2024-04-15 23:39:06 +00:00
martin
95e67a7b1d fix(web): album description height (#8818)
fix: album description height
2024-04-15 19:37:47 -04:00
renovate[bot]
3deaaf14c0 fix(deps): update dependency reflect-metadata to ^0.2.0 (#8784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 16:02:49 -07:00
martin
084a97a77a fix(web): delete trashed item (#8821)
* fix: delete trashed item

* fix: simplify
2024-04-15 16:21:54 -04:00
yparitcher
ed74213c63 feat(server): server host binding (#8800)
* Allow setting the host address for the server & microservices

Default to listen on all interfaces as per the current behavior.

* (Docs) format: fix lint
2024-04-15 14:24:13 -04:00
martin
7ce1662b05 fix(web): remove query parameter when clearing search (#8817)
fix: remove query parameter when clearing search
2024-04-15 18:20:32 +00:00
aviv926
f959f2de85 docs: community guides (#8812)
* Community Guides

* typo

* chore: view guide

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-04-15 17:05:03 +00:00
aviv926
07716bbff7 docs: files custom locations (#8627)
* Files Custom Locations

* minimize

* Easier maintenance

* simplify information

* default .env

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-15 12:33:04 +00:00
Ethan Margaillan
0f74b17000 fix(web): fix scrollbar not allowing the user to go fully top or bottom (#8637)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-15 12:21:47 +00:00
Alex
3c7f70ec30 feat(mobile): haptic feedback setting (#8723)
* feat(mobile): haptic feedback testing

* linting
2024-04-15 07:50:47 +02:00
Kevin Huang
85df3f1e99 fix(server): external library motion photo video asset handling (#8721)
* added "isExternal" to the getLibraryAssetPaths query

* handleQueueAssetRefresh skip "non external" video asset, closes #8562

* correctly implements live photo deletion for external library

* use "external asset" for external library tests

* minor: external library asset checksum is "path hash" not file hash

* renamed to getExternalLibraryAssetPaths and added isExternal where clause

* generated sql

* reverted leftover change
2024-04-14 19:55:44 -04:00
Ben McCann
a903898781 docs: note that uploads are disabled on demo app (#8786) 2024-04-14 18:12:33 -04:00
Ben
25e1887939 fix(web): focus escaping from modals (#8730)
* fix(web): focus escaping modals

* fix: exclusion pattern modal should initially load with the "Add" button disabled

* fix: simplify conditional statement
2024-04-14 15:57:45 +02:00
renovate[bot]
9c696e4c28 chore(deps): update grafana/grafana docker tag to v10.4.2 (#8731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-13 23:23:43 -04:00
mgabor
87a36846f4 fix(web): ui tweaks (#8757)
* remove height limit from user list for better scrolling

* move slideshow button out from menu so that non-owners can see it #8383

* fix activity covering up video player controls #6191

* prettier

---------

Co-authored-by: mgabor <>
2024-04-13 22:41:00 -04:00
(Moai Emoji)
ded01401f8 chore: added 'logs' field to bug template (#8771)
* added logs field to bug_report.yaml

lots of issues are missing logs, people are not submitting them proactively, so a new field is added

* placement suggestion from @bo0tzz
2024-04-13 15:22:33 -04:00
Matthew Momjian
8aff392275 docs: Add community project (#8759)
Update community-projects.tsx
2024-04-12 22:12:58 -04:00
Jason Rasmussen
14b798fcc4 refactor: library e2e (#8693)
* refactor: library e2e

* migrate and refactor library e2e

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-04-12 21:15:41 +02:00
Alex
97c099e26d fix(mobile): random logout (#8739) 2024-04-12 07:33:26 +02:00
Matthew Momjian
3eb61a9d53 docs: DB queries cleanups (#8740)
* Update database-queries.md

* Update database-queries.md

* Update database-queries.md

* Update postgres-standalone.md
2024-04-12 03:44:35 +00:00
Matthew Momjian
e65b3a8ea0 docs: document type of checksum stored in DB (#8737)
* Update database-queries.md

* Update database-queries.md

* Update database-queries.md

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-12 03:18:51 +00:00
Min Idzelis
1fdbc949d6 Add lightroom plugin to community projects (#8736) 2024-04-12 03:14:31 +00:00
shenlong
605da89425 fix(mobile): show error details in the log when available (#8729) 2024-04-12 03:16:40 +02:00
Ben McCann
0d062b32a8 docs: clarify details of connecting to backend for development (#8727)
* docs: clarify details of connecting to backend for development

* simplify
2024-04-11 18:12:14 +02:00
Alex
a4267ed60f chore(mobile): move language setting to another file (#8726) 2024-04-11 14:26:37 +00:00
Kevin Huang
58346465aa fix(server): link motion photo with existing video asset (#8724)
* added motion photo linking

* added tests
2024-04-11 09:49:21 -04:00
Kevin Huang
ec76e5ef23 fix(server): prevent cross-library motion photo linking, made getByChecksum library specific (#8719)
prevent cross linking
2024-04-11 09:41:30 -04:00
Alex
372fae20d9 light theme 2024-04-08 07:37:25 +02:00
Alex
16543a233b basic ui complete 2024-04-07 20:37:26 -05:00
Alex
73b961f5fa colors 2024-04-07 19:38:23 -05:00
Alex
e0d15c96f1 feat(web): workflow automation ui 2024-04-07 19:18:49 -05:00
388 changed files with 11694 additions and 10564 deletions

View File

@@ -87,6 +87,16 @@ body:
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant logs below. (code formatting is
enabled, no need for backticks)
render: shell
validations:
required: false
- type: textarea
attributes:
label: Additional information

View File

@@ -37,15 +37,15 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "11.0.21+9"
cache: "gradle"
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.19.3"
channel: 'stable'
flutter-version: '3.19.3'
cache: true
- name: Create the Keystore

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.5.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.5.0
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -10,19 +10,6 @@ concurrency:
cancel-in-progress: true
jobs:
server-e2e-jobs:
name: Server (e2e-jobs)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Run e2e tests
run: make server-e2e-jobs
doc-tests:
name: Docs
runs-on: ubuntu-latest
@@ -221,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.16.9'
flutter-version: '3.19.3'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1

View File

@@ -16,9 +16,6 @@ stage:
pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull
server-e2e-jobs:
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core
FROM node:20-alpine3.19@sha256:ec0c413b1d84f3f7f67ec986ba885930c57b5318d2eb3abc6960ee05d4f2eb28 as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

241
cli/package-lock.json generated
View File

@@ -47,15 +47,15 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.101.0",
"version": "1.102.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.4",
"typescript": "^5.4.4"
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -1193,12 +1193,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1230,9 +1224,9 @@
}
},
"node_modules/@types/node": {
"version": "20.12.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz",
"integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==",
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1251,22 +1245,22 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
"integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz",
"integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.5.0",
"@typescript-eslint/type-utils": "7.5.0",
"@typescript-eslint/utils": "7.5.0",
"@typescript-eslint/visitor-keys": "7.5.0",
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.6.0",
"@typescript-eslint/type-utils": "7.6.0",
"@typescript-eslint/utils": "7.6.0",
"@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1286,15 +1280,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
"integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz",
"integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.5.0",
"@typescript-eslint/types": "7.5.0",
"@typescript-eslint/typescript-estree": "7.5.0",
"@typescript-eslint/visitor-keys": "7.5.0",
"@typescript-eslint/scope-manager": "7.6.0",
"@typescript-eslint/types": "7.6.0",
"@typescript-eslint/typescript-estree": "7.6.0",
"@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1314,13 +1308,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
"integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz",
"integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.5.0",
"@typescript-eslint/visitor-keys": "7.5.0"
"@typescript-eslint/types": "7.6.0",
"@typescript-eslint/visitor-keys": "7.6.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1331,15 +1325,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
"integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz",
"integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.5.0",
"@typescript-eslint/utils": "7.5.0",
"@typescript-eslint/typescript-estree": "7.6.0",
"@typescript-eslint/utils": "7.6.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1358,9 +1352,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
"integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz",
"integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==",
"dev": true,
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1371,19 +1365,19 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
"integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz",
"integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.5.0",
"@typescript-eslint/visitor-keys": "7.5.0",
"@typescript-eslint/types": "7.6.0",
"@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"minimatch": "9.0.3",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1399,18 +1393,18 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
"integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
"integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.5.0",
"@typescript-eslint/types": "7.5.0",
"@typescript-eslint/typescript-estree": "7.5.0",
"semver": "^7.5.4"
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.6.0",
"@typescript-eslint/types": "7.6.0",
"@typescript-eslint/typescript-estree": "7.6.0",
"semver": "^7.6.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1424,13 +1418,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
"integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz",
"integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.5.0",
"eslint-visitor-keys": "^3.4.1"
"@typescript-eslint/types": "7.6.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1447,9 +1441,9 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz",
"integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.0.tgz",
"integrity": "sha512-1igVwlcqw1QUMdfcMlzzY4coikSIBN944pkueGi0pawrX5I5Z+9hxdTR+w3Sg6Q3eZhvdMAs8ZaF9JuTG1uYOQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -1464,24 +1458,23 @@
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
"test-exclude": "^6.0.0",
"v8-to-istanbul": "^9.2.0"
"test-exclude": "^6.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.4.0"
"vitest": "1.5.0"
}
},
"node_modules/@vitest/expect": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
"integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz",
"integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.4.0",
"@vitest/utils": "1.4.0",
"@vitest/spy": "1.5.0",
"@vitest/utils": "1.5.0",
"chai": "^4.3.10"
},
"funding": {
@@ -1489,12 +1482,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
"integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz",
"integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.4.0",
"@vitest/utils": "1.5.0",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -1530,9 +1523,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
"integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz",
"integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -1544,9 +1537,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
"integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz",
"integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -1556,9 +1549,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
"integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz",
"integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -1909,12 +1902,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/core-js-compat": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
@@ -3139,9 +3126,9 @@
}
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -4258,9 +4245,9 @@
"dev": true
},
"node_modules/tinypool": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
"integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
"integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
"dev": true,
"engines": {
"node": ">=14.0.0"
@@ -4368,9 +4355,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -4431,20 +4418,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
"integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
"@types/istanbul-lib-coverage": "^2.0.1",
"convert-source-map": "^2.0.0"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -4511,9 +4484,9 @@
}
},
"node_modules/vite-node": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
"integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz",
"integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -4552,16 +4525,16 @@
}
},
"node_modules/vitest": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
"integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz",
"integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.4.0",
"@vitest/runner": "1.4.0",
"@vitest/snapshot": "1.4.0",
"@vitest/spy": "1.4.0",
"@vitest/utils": "1.4.0",
"@vitest/expect": "1.5.0",
"@vitest/runner": "1.5.0",
"@vitest/snapshot": "1.5.0",
"@vitest/spy": "1.5.0",
"@vitest/utils": "1.5.0",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -4573,9 +4546,9 @@
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
"tinybench": "^2.5.1",
"tinypool": "^0.8.2",
"tinypool": "^0.8.3",
"vite": "^5.0.0",
"vite-node": "1.4.0",
"vite-node": "1.5.0",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -4590,8 +4563,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.4.0",
"@vitest/ui": "1.4.0",
"@vitest/browser": "1.5.0",
"@vitest/ui": "1.5.0",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -97,7 +97,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database:
container_name: immich_postgres

View File

@@ -54,7 +54,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always
database:
@@ -88,7 +88,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -58,7 +58,7 @@ services:
redis:
container_name: immich_redis
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always
database:

View File

@@ -1,6 +1,6 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation

View File

@@ -6,7 +6,7 @@
The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
### How can I see list of all users in Immich?
### How can I see a list of all users in Immich?
You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server.
@@ -24,14 +24,14 @@ You can see the list of all users by running [list-users](/docs/administration/s
### I cannot log into the application after an update. What can I do?
First, verify that the mobile app and server are both running the same version (major and minor).
Verify that the mobile app and server are both running the same version (major and minor).
:::note
App store updates sometimes take longer because the stores (Google play store and Apple app store)
need to approve the update first which may take some time.
App store updates sometimes take longer because the stores (Google Play Store and Apple App Store)
need to approve the update first, and it can take some time.
:::
If you still cannot login to the app, try the following:
If you still cannot log in to the app, try the following:
- Check the mobile logs
- Make sure login credentials are correct by logging in on the web app
@@ -40,6 +40,11 @@ If you still cannot login to the app, try the following:
## Assets
### Does Immich change the file?
No, Immich does not touch the original file under any circumstances,
all edited metadata are saved in the companion sidecar file and the database.
### Can I add my existing photo library?
Yes, with an [External Library](/docs/features/libraries.md).
@@ -50,11 +55,11 @@ Template changes will only apply to _new_ assets. To retroactively apply the tem
### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy (such as nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also check the disk space of your reverse proxy, in some cases proxies cache requests to disk before passing them on, and if disk space runs out the request fails.
This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
### Why are some photos stored in the file system with the wrong date?
There are a few different scenarios that can lead to this situation. The solution is to run the storage migration job again. The job is only _automatically_ run once per asset, after upload. If metadata extraction originally failed, the jobs were cleared/cancelled, etc. the job may not have run automatically the first time.
There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time.
### How can I hide photos from the timeline?
@@ -68,23 +73,27 @@ See [Backup and Restore](/docs/administration/backup-and-restore.md).
No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348).
### Does Immich support filtering of NSFW images?
### Does Immich support the filtering of NSFW images?
No, it currently does not. There is an [open feature request on Github](https://github.com/immich-app/immich/discussions/2451).
### Why are there so many thumbnail generation jobs?
There are three thubmanil jobs for each asset:
There are three thumbnail jobs for each asset:
- Blurred (thumbhash)
- Small (webp)
- Large (jpeg)
- Preview (Webp)
- Thumbnail (Jpeg)
Also, there are additional jobs for person (face) thumbnails.
### Why do files from WhatsApp not appear with the correct date?
Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527).
### What happens if an asset exists in more than one account?
There are no requirements for assets to be unique across users. If multiple users upload the same image they are processed as if they were distinct assets and jobs run and thumbnails are generated accordingly.
There are no requirements for assets to be unique across users. If multiple users upload the same image, it is processed as if it were a distinct asset, and jobs run and thumbnails are generated accordingly.
### Why do HDR videos appear pale in Immich player but look normal after download?
@@ -96,40 +105,36 @@ Immich always keeps your original files. Alongside that, it generates a transcod
### How can I delete transcoded videos without deleting the original?
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options, or for all assets by running transcoding jobs for all assets from the administration page.
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary and then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options or for all assets by running transcoding jobs for all assets from the administration page.
To update the transcode policy, navigate to Administration > Video Transcoding Settings > Transcoding Policy and select a policy from the drop-down. This policy will determine whether an existing transcode will be deleted or overwritten in the transcoding job. If a video should be transcoded according to this policy, an existing transcode is overwritten. If not, then it is deleted.
:::note
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format". If an asset was only transcoded due to its resolution, then running a transcoding job for it will now delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary as a result. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are all unnecessary.
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format." If an asset was only transcoded due to its resolution, running a transcoding job for it will delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are unnecessary.
:::
### Is it possible to compress images during backup?
No. Our golden rule is that the original assets should always be untouched, so we don't think this feature is a good fit for Immich.
No. Our design principle is that the original assets should always be untouched.
### How can I move all data (photos, persons, albums) from one user to another?
This is not officially supported, but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.
:::warning
This is an advanced operation. If you can't do it with the steps described here, this is not for you.
:::
This is not officially supported but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add, for example, an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file so that you can use a web interface.
<details>
<summary>Steps</summary>
1. **MAKE A BACKUP** - See [backup and restore](/docs/administration/backup-and-restore.md).
2. Find the id of both the 'source' and the 'destination' user (it's the id column in the users table)
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
3. Three tables need to be updated:
```sql
// reassign albums
// Reassign albums
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// reassign people
// Reassign people
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
// reassign assets
@@ -159,7 +164,7 @@ No, not yet. For updates on this planned feature, follow the [GitHub discussion]
### Can I add an external library while keeping the existing album structure?
We haven't put in an official mechanism to create albums from external libraries at the moment, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
We haven't implemented an official mechanism for creating albums from external libraries, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
### What happens to duplicates in external libraries?
@@ -171,7 +176,7 @@ Duplicate checking only exists for upload libraries, using the file hash. Furthe
### How does smart search work?
Immich uses CLIP models, for more information about CLIP and its capabilities read about it [here](https://openai.com/research/clip).
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
### How does facial recognition work?
@@ -189,33 +194,31 @@ However, disabling all jobs will not disable the machine learning service itself
### I'm getting errors about models being corrupt or failing to download. What do I do?
You can delete the model cache volume, which is where models are downloaded to. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface](https://huggingface.co/immich-app) and place them in the cache folder.
### Why did Immich decide to remove object detection?
The feature added keywords to images for metadata search, but wasn't used for smart search. Smart search made it unnecessary as it isn't limited to exact keywords. Combined with it causing crashes on some devices, using many dependencies and causing user confusion as to how search worked, it was better to remove the job altogether.
For more info see [here](https://github.com/immich-app/immich/pull/5903)
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface][huggingface] and place them in the cache folder.
### Can I use a custom CLIP model?
No, this is not supported. Only models listed in the [Huggingface](https://huggingface.co/immich-app) page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
No, this is not supported. Only models listed in the [Huggingface][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
### I want to be able to search in other languages besides English. How can I do that?
You can change to a multilingual model listed [here](https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7) by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model. Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages.
:::note
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list](https://huggingface.co/immich-app).
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list][huggingface].
:::
### Does Immich support Facial Recognition for videos ?
### Does Immich support Facial Recognition for videos?
Immich's machine learning feature operate on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
Immich's machine learning feature operates on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
Scanning the entire video for faces may be implemented in the future.
### Does Immich have animal recognition?
No.
:::tip
You can use [Smart Search](/docs/features/smart-search.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
:::
### I'm getting a lot of "faces" that aren't faces, what can I do?
@@ -248,8 +251,8 @@ The initial backup is the most intensive due to the number of jobs running. The
- Lower the job concurrency for these jobs to 1.
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- You _must_ re-run the Face Detection job for all images after this for facial recognition on new images to work properly.
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for how you can disable machine learning.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit the amount of CPU and RAM usage?
@@ -280,13 +283,17 @@ On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond thi
Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server.
More detail can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
More details can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
:::
### Why is Immich using so much of my CPU?
When a large amount of assets are uploaded to Immich it makes sense that the CPU and RAM will be heavily used due to machine learning work and creating image thumbnails.
Once this process completes, the percentage of CPU usage will drop to around 3-5% usage
When a large number of assets are uploaded to Immich, it makes sense that the CPU and RAM will be heavily used for machine learning work and creating image thumbnails.
Once this process is completed, the percentage of CPU usage will drop to around 3-5% usage
### My server shows Server Status Offline | Version Unknown what can I do?
You need to enable Websocket on your reverse proxy.
---
@@ -311,7 +318,7 @@ For a further hardened system, you can add the following block to every containe
```yaml
security_opt:
# Prevent escalation of privileges after container is started
# Prevent escalation of privileges after the container is started
- no-new-privileges:true
cap_drop:
# Prevent access to raw network traffic
@@ -322,7 +329,7 @@ cap_drop:
Data for Immich comes in two forms:
1. **Metadata** stored in a postgres database, persisted via the `pg_data` volume
1. **Metadata** stored in a Postgres database, persisted via the `pg_data` volume
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations).
To remove the **Metadata** you can stop Immich and delete the volume.
@@ -359,3 +366,5 @@ If your version of Immich is below 1.92.0 and the crash occurs after logs about
### Why does Immich log migration errors on startup?
Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`.
[huggingface]: https://huggingface.co/immich-app

View File

@@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Hostname
- `https://immich.example.com/auth/login`)
- `https://immich.example.com/user-settings`)
- `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`
## Enable OAuth

View File

@@ -10,9 +10,11 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
## Prerequisites
You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
:::note
Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported.
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
:::
@@ -30,6 +32,10 @@ DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify'
```
:::info
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
:::
## With superuser permission
Typically Immich expects superuser permission in the database, which you can grant by running `ALTER USER <immichdbusername> WITH SUPERUSER;` at the `psql` console. If you prefer not to grant superuser permissions, follow the instructions in the next section.
@@ -50,8 +56,7 @@ ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
CREATE EXTENSION vectors;
CREATE EXTENSION earthdistance CASCADE;
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
COMMIT;
```
@@ -63,4 +68,6 @@ When installing a new version of pgvecto.rs, you will need to manually update th
#### Permission denied for view
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat to <immichdbusername>;`.
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html

View File

@@ -37,9 +37,7 @@ You can set the scanning interval using the preset or cron format. For more info
## Logging
By default logs are set to record at the log level, the network administrator can choose a deeper or lower level of logs according to his decision or according to the needs required by the Immich support team.
Here you can [learn about the different error levels](https://sematext.com/blog/logging-levels/).
The default Immich log level is `Log` (commonly known as `Info`). The Immich administrator can choose a higher or lower log level according to personal preference or as requested by the Immich support team.
## Machine Learning Settings

View File

@@ -0,0 +1,12 @@
# Community Guides
This page lists community guides that are written around Immich, but not officially supported by the development team.
:::warning
This list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.
:::
import CommunityGuides from '../src/components/community-guides.tsx';
import React from 'react';
<CommunityGuides />

View File

@@ -9,7 +9,7 @@ When contributing code through a pull request, please check the following:
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
- [ ] `npm test` (unit tests)
:::tip
:::tip AIO
Run all web checks with `npm run check:all`
:::
@@ -20,10 +20,14 @@ Run all web checks with `npm run check:all`
- [ ] `npm run check` (Type checking via `tsc`)
- [ ] `npm test` (unit tests)
:::tip
:::tip AIO
Run all server checks with `npm run check:all`
:::
:::info Auto Fix
You can use `npm run __:fix` to potentially correct some issues automatically for `npm run format` and `lint`.
:::
## OpenAPI
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details.

View File

@@ -16,7 +16,7 @@ Thanks for being interested in contributing 😊
## Environment
### Server and web app
### Services
This environment includes the services below. Additional details are available in each service's README.
@@ -28,7 +28,7 @@ This environment includes the services below. Additional details are available i
All the services are packaged to run as with single Docker Compose command.
### Instructions
### Server and web apps
1. Clone the project repo.
2. Run `cp docker/example.env docker/.env`.
@@ -47,13 +47,7 @@ You can access the web from `http://your-machine-ip:2283` or `http://localhost:2
**Note:** the "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
### Connect to a remote backend
#### Connect web to a remote backend
If you only want to do web development connected to an existing, remote backend, follow these steps:
@@ -66,6 +60,14 @@ If you only want to do web development connected to an existing, remote backend,
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
```
### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
## IDE setup
### Lint / format extensions

View File

@@ -8,15 +8,24 @@ Unit are run by calling `npm run test` from the `server` directory.
### End to end tests
The backend has two end-to-end test suites that can be called with the following two commands from the project root directory:
The e2e tests can be run by first starting up a test production environment via:
- `make server-e2e-api`
- `make server-e2e-jobs`
```bash
make e2e
```
#### API (e2e)
Once the test environment is running, the e2e tests can be run via:
The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints.
```bash
cd e2e/
npm test
```
#### Jobs (e2e)
The tests check various things including:
The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated.
- Authentication and authorization
- Query param, body, and url validation
- Response codes
- Thumbnail generation
- Metadata extraction
- Library scanning

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -161,7 +161,7 @@ The christmas trip library will now be scanned in the background. In the meantim
- Click on Create External Library.
:::info Note
:::note
If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release.
:::
@@ -175,3 +175,14 @@ If you get an error here, please rename the other external library to something
- Click on Scan Library Files
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
### Set Custom Scan Interval
:::note
Only an admin can do this.
:::
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
<img src={require('./img/library-custom-scan-interval.png').default} width="75%" title='Set custom scan interval for external library' />

View File

@@ -10,7 +10,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
## Supported Backends
- ARM NN (Mali)
- CUDA (NVIDIA)
- CUDA (NVIDIA) Note: It is supported with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher
- OpenVINO (Intel)
## Limitations

View File

@@ -3,7 +3,7 @@
Immich supports a number of image and video formats, the most common of which are outlined here.
:::note
For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
For the full list, refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
:::
## Image formats

View File

@@ -0,0 +1,50 @@
# Files Custom Locations
This guide explains storing generated and raw files with docker's volume mount in different locations.
:::note Backup
It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools.
:::
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
```diff title=".env"
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- {UPLOAD_LOCATION}=./library
+ {UPLOAD_LOCATION}=/custom/location/on/your/system/
+ {THUMB_LOCATION}=/custom/location/on/your/system/
+ {ENCODED_VIDEO_LOCATION}=/custom/location/on/your/system/
...
```
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers.
```diff title="docker-compose.yml"
services:
immich-server:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
- /etc/localtime:/etc/localtime:ro
...
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
- /etc/localtime:/etc/localtime:ro
```
Restart Immich to register the changes.
```
docker compose down
docker compose up -d
```
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.

View File

@@ -45,5 +45,5 @@ Open pgAdmin and click "Add New Server".
Click on "Save" to connect to the Immich database.
:::tip
View [Database Queries](https://immich.app/docs/guides/database-queries/) for common database queries.
View [Database Queries](/docs/guides/database-queries/) for common database queries.
:::

View File

@@ -17,7 +17,7 @@ The `"originalFileName"` column is the name of the file at time of upload, inclu
:::
```sql title="Find by original filename"
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848';
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848.jpg';
SELECT * FROM "assets" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_
SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle
```
@@ -27,21 +27,30 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
```
```sql title="Find by checksum" (sha1)
:::note
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
:::
```sql title="Find by checksum (SHA-1)"
SELECT encode("checksum", 'hex') FROM "assets";
SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex');
```
```sql title="Live photos"
SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL;
```
```sql title="Without metadata"
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
SELECT "assets".* FROM "exif"
LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId"
WHERE "exif"."assetId" IS NULL;
```
```sql title="size < 100,000 bytes, smallest to largest"
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
SELECT * FROM "assets"
JOIN "exif" ON "assets"."id" = "exif"."assetId"
WHERE "exif"."fileSizeInByte" < 100000
ORDER BY "exif"."fileSizeInByte" ASC;
```
```sql title="Without thumbnails"
@@ -54,20 +63,14 @@ SELECT * FROM "assets" WHERE "assets"."type" = 'IMAGE';
```
```sql title="Count by type"
SELECT "assets"."type", count(*) FROM "assets" GROUP BY "assets"."type";
SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type";
```
```sql title="Count by type (per user)"
SELECT
"users"."email", "assets"."type", COUNT(*)
FROM
"assets"
JOIN
"users" ON "assets"."ownerId" = "users"."id"
GROUP BY
"assets"."type", "users"."email"
ORDER BY
"users"."email";
SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets"
JOIN "users" ON "assets"."ownerId" = "users"."id"
GROUP BY "assets"."type", "users"."email"
ORDER BY "users"."email";
```
```sql title="Failed file movements"
@@ -76,7 +79,7 @@ SELECT * FROM "move_history";
## Users
```sql title="List"
```sql title="List all users"
SELECT * FROM "users";
```
@@ -87,3 +90,9 @@ SELECT "key", "value" FROM "system_config";
```
(Only used when not using the [config file](/docs/install/config-file))
## Persons
```sql title="Delete person and unset it for the faces it was associated with"
DELETE FROM person WHERE name = 'PersonNameHere';
```

View File

@@ -14,12 +14,6 @@ Edit `docker-compose.yml` to add two new mount points under `volumes:`
- ${EXTERNAL_PATH}:/usr/src/app/external
```
```
immich-microservices:
volumes:
- ${EXTERNAL_PATH}:/usr/src/app/external
```
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:

View File

@@ -6,10 +6,14 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Copy the following `docker-compose.yml` to your ML system.
- Start the container by running `docker compose up -d`.
:::note Info
:::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
:::
:::note
The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be in the same folder if trying to use [hardware acceleration](/docs/features/ml-hardware-acceleration).
:::
```yaml
version: '3.8'

View File

@@ -120,7 +120,8 @@ The default configuration looks like this:
"previewFormat": "jpeg",
"previewSize": 1440,
"quality": 80,
"colorspace": "p3"
"colorspace": "p3",
"extractEmbedded": false
},
"newVersionCheck": {
"enabled": true

View File

@@ -7,7 +7,8 @@ import ExampleEnv from '!!raw-loader!../../../docker/example.env';
# Docker Compose [Recommended]
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
Immich requires Docker Compose version 2.x.
### Step 1 - Download the required files
@@ -66,7 +67,7 @@ docker compose up -d
```
:::info Docker version
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository).
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository][docker-repo].
Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04 it will fail in a different way:
@@ -83,27 +84,31 @@ For more information on how to use the application, please refer to the [Post In
:::
:::note GitHub Authentication
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here][container-auth]).
:::
### Step 4 - Upgrading
:::danger Breaking Changes
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created).
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here][breaking].
:::
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
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][releases], the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
```bash title="Upgrade Immich"
docker compose pull && docker compose up -d
```
:::caution Automatic Updates
Immich is currently under heavy development, which means you can expect [breaking changes](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created) and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
:::
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
[watchtower]: https://containrrr.dev/watchtower/
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
[releases]: https://github.com/immich-app/immich/releases
[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

View File

@@ -41,19 +41,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
:::tip
`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`.
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
:::
## Ports
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-------: | :--------------- |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-------: | :-------------------- |
| `HOST` | Host | `0.0.0.0` | server, microservices |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## Database
@@ -71,7 +72,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
:::info
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
:::
@@ -90,7 +91,7 @@ When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
:::info
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis](https://ioredis.readthedocs.io/en/latest/API/) documentation.
More info can be found in the upstream [ioredis][redis-api] documentation.
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
@@ -158,7 +159,7 @@ Other machine learning parameters can be tuned from the admin UI.
## Docker Secrets
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
The following variables support the use of [Docker secrets][docker-secrets] for additional security.
To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of
the `_FILE` variable should be set to the path of a file containing the variable value.
@@ -172,8 +173,14 @@ the `_FILE` variable should be set to the path of a file containing the variable
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
\*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for
\*1: See the [official documentation][docker-secrets-docs] for
details on how to use Docker Secrets in the Postgres image.
\*2: See [this comment](https://github.com/docker-library/redis/issues/46#issuecomment-335326234) for an example of how
\*2: See [this comment][docker-secrets-example] for an example of how
to use use a Docker secret for the password in the Redis container.
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

View File

@@ -6,7 +6,7 @@ sidebar_position: 40
You can deploy Immich on Kubernetes using [the official Helm chart](https://github.com/immich-app/immich-charts/tree/main/charts/immich).
If you want examples of how other people run Immich on Kubernetes, using the official chart or otherwise, you can find them at https://nanne.dev/k8s-at-home-search/#/immich.
You can view some [examples](https://kubesearch.dev/#/immich) of how other people run Immich on Kubernetes, using the official chart or otherwise.
:::caution DNS in Alpine containers
Immich makes use of Alpine container images. These can encounter [a DNS resolution bug](https://stackoverflow.com/a/65593511) on Kubernetes clusters if the host

View File

@@ -1,4 +1,4 @@
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level.
Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user).
:::note new version
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).

View File

@@ -1,3 +1,3 @@
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user can change their password after logging in to the application for the first time.
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user has to change their password after logging in to the application for the first time. The system administrator can disable this option by unchecking the option "Require user to change password on first login" in the user registration interface.
<img src={require('./img/create-new-user.png').default} title='Admin Registration' />
<img src={require('./img/create-new-user.png').default} width="90%" title='New User Registration' />

View File

@@ -144,6 +144,10 @@ const config = {
label: 'Discord',
href: 'https://discord.com/invite/D8JsnBEuKb',
},
{
label: 'Reddit',
href: 'https://www.reddit.com/r/immich/',
},
],
},
{
@@ -157,6 +161,10 @@ const config = {
label: 'GitHub',
href: 'https://github.com/immich-app/immich',
},
{
label: 'YouTube',
href: 'https://www.youtube.com/@immich-app',
},
],
},
],

View File

@@ -16125,9 +16125,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -0,0 +1,71 @@
import Link from '@docusaurus/Link';
import React from 'react';
interface CommunityGuidesProps {
title: string;
description: string;
url: string;
}
const guides: CommunityGuidesProps[] = [
{
title: 'Cloudflare Tunnels with SSO/OAuth',
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
url: 'https://github.com/immich-app/immich/discussions/8299',
},
{
title: 'Database backup in Truenas',
description: `Create a database backup with pgAdmin in Truenas.`,
url: 'https://github.com/immich-app/immich/discussions/8809',
},
{
title: 'Unraid backup scripts',
description: `Back up your assets in Unarid with a pre-prepared script.`,
url: 'https://github.com/immich-app/immich/discussions/8416',
},
{
title: 'Sync folders with albums',
description: `synchronize folders in imported library with albums having the folders name.`,
url: 'https://github.com/immich-app/immich/discussions/3382',
},
{
title: 'Podman/Quadlets Install',
description: 'Documentation for simple podman setup using quadlets.',
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
return (
<section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
<div className="flex flex-col gap-2">
<p className="m-0 items-start flex gap-2">
<span>{title}</span>
</p>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">
<a href={url}>{url}</a>
</p>
</div>
<div className="flex">
<Link
className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to={url}
>
View Guide
</Link>
</div>
</section>
);
}
export default function CommunityGuides(): JSX.Element {
return (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{guides.map((guides) => (
<CommunityGuide {...guides} />
))}
</div>
);
}

View File

@@ -34,9 +34,29 @@ const projects: CommunityProjectProps[] = [
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
},
{
title: 'Podman/Quadlets Install',
description: 'Documentation for simple podman setup using quadlets.',
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
title: 'Lightroom Publisher: mi.Immich.Publisher',
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
url: 'https://github.com/midzelis/mi.Immich.Publisher',
},
{
title: 'Immich Duplicate Finder',
description: 'Webapp that uses machine learning to identify near-duplicate images.',
url: 'https://github.com/vale46n1/immich_duplicate_finder',
},
{
title: 'Immich-Tiktok-Remover',
description: 'Script to search for and remove TikTok videos from your Immich library.',
url: 'https://github.com/mxc2/immich-tiktok-remover',
},
{
title: 'Immich Android TV',
description: 'Unofficial Immich Android TV app.',
url: 'https://github.com/giejay/Immich-Android-TV',
},
{
title: 'Powershell Module PSImmich',
description: 'Powershell Module for the Immich API',
url: 'https://github.com/hanpq/PSImmich',
},
];

View File

@@ -51,12 +51,22 @@ import {
mdiVideo,
mdiWeb,
mdiScaleBalance,
mdiMagnifyScan,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [
{
icon: mdiMagnifyScan,
description: 'Advanced search with filters by date, location and more',
title: 'Search enhancement with advanced filters',
release: 'v1.95.0',
tag: 'v1.95.0',
date: new Date(2024, 1, 20),
dateType: DateType.RELEASE,
},
{
icon: mdiScaleBalance,
description: 'Immich switches to AGPLv3 license',

View File

@@ -36,7 +36,7 @@ services:
<<: *server-common
redis:
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

847
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.101.0",
"version": "1.102.3",
"description": "",
"main": "index.js",
"type": "module",
@@ -43,6 +43,7 @@
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4",
"typescript": "^5.3.3",
"utimes": "^5.2.1",
"vitest": "^1.3.0"
}
}

View File

@@ -572,6 +572,22 @@ describe('/asset', () => {
}
const tests = [
{
input: 'formats/avif/8bit-sRGB.avif',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.avif',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1617,
fileSizeInByte: 862_424,
latitude: null,
longitude: null,
},
},
},
{
input: 'formats/jpg/el_torcal_rocks.jpg',
expected: {
@@ -596,6 +612,22 @@ describe('/asset', () => {
},
},
},
{
input: 'formats/jxl/8bit-sRGB.jxl',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.jxl',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1440,
fileSizeInByte: 1_780_777,
latitude: null,
longitude: null,
},
},
},
{
input: 'formats/heic/IMG_2682.heic',
expected: {
@@ -681,6 +713,80 @@ describe('/asset', () => {
},
},
},
{
input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '4_3.rw2',
resized: true,
fileCreatedAt: '2018-05-10T08:42:37.842Z',
exifInfo: {
make: 'Panasonic',
model: 'DMC-GH4',
exifImageHeight: 3456,
exifImageWidth: 4608,
exposureTime: '1/100',
fNumber: 3.2,
focalLength: 35,
iso: 400,
fileSizeInByte: 19_587_072,
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '12bit-compressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-09-27T10:51:44.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-6300',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1/320',
fNumber: 8,
focalLength: 97,
iso: 100,
lensModel: 'E PZ 18-105mm F4 G OSS',
fileSizeInByte: 25_001_984,
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '14bit-uncompressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-01-08T15:08:01.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-7M2',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1.3',
fNumber: 22,
focalLength: 25,
iso: 100,
lensModel: 'E 25mm F2',
fileSizeInByte: 49_512_448,
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
];
for (const { input, expected } of tests) {
@@ -816,15 +922,15 @@ describe('/asset', () => {
});
it('should not include gps data for webp thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: locationAsset.id,
});
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/webp');

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto } from 'src/fixtures';
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
@@ -112,70 +112,29 @@ describe('/auth/*', () => {
const cookies = headers['set-cookie'];
expect(cookies).toHaveLength(3);
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
});
});
describe('GET /auth/devices', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/auth/devices');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app)
.get('/auth/devices')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([deviceDto.current]);
});
});
describe('DELETE /auth/devices', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/auth/devices`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout all devices (except the current one)', async () => {
for (let i = 0; i < 5; i++) {
await login({ loginCredentialDto: loginDto.admin });
}
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
});
it('should throw an error for a non-existent device id', async () => {
const { status, body } = await request(app)
.delete(`/auth/devices/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
});
it('should logout a device', async () => {
const [device] = await getAuthDevices({
headers: asBearerAuth(admin.accessToken),
});
const { status } = await request(app)
.delete(`/auth/devices/${device.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const response = await request(app)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.body).toEqual(errorDto.invalidToken);
expect(response.status).toBe(401);
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
`immich_access_token=${token}`,
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
'immich_auth_type=password',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'HttpOnly',
'SameSite=Lax',
]);
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
'immich_is_authenticated=true',
'Max-Age=34560000',
'Path=/',
expect.stringContaining('Expires='),
'SameSite=Lax',
]);
});
});

View File

@@ -6,12 +6,13 @@ import {
getAllLibraries,
scanLibrary,
} from '@immich/sdk';
import { existsSync, rmdirSync } from 'node:fs';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
import request from 'supertest';
import { utimes } from 'utimes';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
@@ -26,23 +27,21 @@ describe('/library', () => {
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
await utils.resetAdminConfig(admin.accessToken);
user = await utils.userSetup(admin.accessToken, userDto.user1);
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
websocket = await utils.connectWebsocket(admin.accessToken);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
utils.resetTempFolder();
});
beforeEach(() => {
utils.resetEvents();
const tempDir = `${testAssetDir}/temp`;
if (existsSync(tempDir)) {
rmdirSync(tempDir, { recursive: true });
}
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
});
describe('GET /library', () => {
@@ -357,95 +356,6 @@ describe('/library', () => {
});
});
describe('DELETE /library/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not delete the last upload library', async () => {
const libraries = await getAllLibraries(
{ $type: LibraryType.Upload },
{ headers: asBearerAuth(admin.accessToken) },
);
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
// delete all but the last upload library
for (const library of adminLibraries) {
const { status } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
const { status, body } = await request(app)
.delete(`/library/${lastLibrary.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
expect(status).toBe(400);
});
it('should delete an external library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
});
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
id: library.id,
}),
]),
);
});
it('should delete an external library with assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
id: library.id,
}),
]),
);
// ensure no files get deleted
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
});
});
describe('GET /library/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
@@ -549,6 +459,150 @@ describe('/library', () => {
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
});
it('should offline missing files', async () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
originalFileName: 'assetC.png',
}),
]),
);
});
describe('with refreshModifiedFiles=true', () => {
it('should reimport modified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
});
});
describe('with refreshAllFiles=true', () => {
it('should reimport all files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshAllFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
});
});
@@ -559,6 +613,72 @@ describe('/library', () => {
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2);
});
it('should not remove online files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: assetsBefore } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets).toEqual(assetsBefore);
});
});
describe('POST /library/:id/validate', () => {
@@ -608,4 +728,93 @@ describe('/library', () => {
});
});
});
describe('DELETE /library/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not delete the last upload library', async () => {
const libraries = await getAllLibraries(
{ $type: LibraryType.Upload },
{ headers: asBearerAuth(admin.accessToken) },
);
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
// delete all but the last upload library
for (const library of adminLibraries) {
const { status } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
const { status, body } = await request(app)
.delete(`/library/${lastLibrary.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
expect(status).toBe(400);
});
it('should delete an external library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
});
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
id: library.id,
}),
]),
);
});
it('should delete an external library with assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
id: library.id,
}),
]),
);
// ensure no files get deleted
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
});
});
});

View File

@@ -1,4 +1,4 @@
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
@@ -32,6 +32,9 @@ describe('/search', () => {
let assetGlarus: AssetFileUploadResponseDto;
let assetSprings: AssetFileUploadResponseDto;
let assetLast: AssetFileUploadResponseDto;
let cities: string[];
let states: string[];
let countries: string[];
beforeAll(async () => {
await utils.resetDatabase();
@@ -79,7 +82,7 @@ describe('/search', () => {
}
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
const cities = [
const coordinates = [
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
{ latitude: 63.0695, longitude: -151.0074 }, // denali
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
@@ -101,7 +104,7 @@ describe('/search', () => {
];
const updates = assets.map((asset, i) =>
updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }),
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
);
await Promise.all(updates);
@@ -133,6 +136,12 @@ describe('/search', () => {
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
}, 30_000);
afterAll(async () => {
@@ -452,21 +461,7 @@ describe('/search', () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=country')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Cuba',
'France',
'Georgia',
'Germany',
'Ghana',
'Japan',
'Morocco',
"People's Republic of China",
'Russian Federation',
'Singapore',
'Spain',
'Switzerland',
'United States of America',
]);
expect(body).toEqual(countries);
expect(status).toBe(200);
});
@@ -474,23 +469,7 @@ describe('/search', () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=state')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Accra, Greater Accra',
'Berlin',
'Glarus, Glarus',
'Havana',
'Marrakech, Marrakesh-Safi',
'Mesa County, Colorado',
'Neshoba County, Mississippi',
'New York',
'Page County, Virginia',
'Paris, Île-de-France',
'Province of Córdoba, Andalusia',
'Shanghai Municipality, Shanghai',
'St.-Petersburg',
'Tbilisi',
'Tokyo',
]);
expect(body).toEqual(states);
expect(status).toBe(200);
});
@@ -498,24 +477,7 @@ describe('/search', () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=city')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Accra',
'Berlin',
'Glarus',
'Havana',
'Marrakesh',
'Montalbán de Córdoba',
'New York City',
'Palisade',
'Paris',
'Philadelphia',
'Saint Petersburg',
'Shanghai',
'Singapore',
'Stanley',
'Tbilisi',
'Tokyo',
]);
expect(body).toEqual(cities);
expect(status).toBe(200);
});

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
@@ -162,19 +162,4 @@ describe('/server-info', () => {
});
});
});
describe('POST /server-info/admin-onboarding', () => {
it('should set admin onboarding', async () => {
const config = await getServerConfig({});
expect(config.isOnboarded).toBe(false);
const { status } = await request(app)
.post('/server-info/admin-onboarding')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const newConfig = await getServerConfig({});
expect(newConfig.isOnboarded).toBe(true);
});
});
});

View File

@@ -0,0 +1,75 @@
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { deviceDto, errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
describe('/sessions', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe('GET /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/sessions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([deviceDto.current]);
});
});
describe('DELETE /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/sessions`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout all devices (except the current one)', async () => {
for (let i = 0; i < 5; i++) {
await login({ loginCredentialDto: loginDto.admin });
}
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
const { status } = await request(app).delete(`/sessions`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
});
it('should throw an error for a non-existent device id', async () => {
const { status, body } = await request(app)
.delete(`/sessions/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
});
it('should logout a device', async () => {
const [device] = await getSessions({
headers: asBearerAuth(admin.accessToken),
});
const { status } = await request(app)
.delete(`/sessions/${device.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const response = await request(app)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.body).toEqual(errorDto.invalidToken);
expect(response.status).toBe(401);
});
});
});

View File

@@ -0,0 +1,76 @@
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/server-info', () => {
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('POST /system-metadata/admin-onboarding', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.post('/system-metadata/admin-onboarding')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ isOnboarded: true });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should set admin onboarding', async () => {
const config = await getServerConfig({});
expect(config.isOnboarded).toBe(false);
const { status } = await request(app)
.post('/system-metadata/admin-onboarding')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ isOnboarded: true });
expect(status).toBe(204);
const newConfig = await getServerConfig({});
expect(newConfig.isOnboarded).toBe(true);
});
});
describe('GET /system-metadata/reverse-geocoding-state', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/system-metadata/reverse-geocoding-state')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get the reverse geocoding state', async () => {
const { status, body } = await request(app)
.get('/system-metadata/reverse-geocoding-state')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
lastUpdate: expect.any(String),
lastImportFileName: 'cities500.txt',
});
});
});
});

View File

@@ -0,0 +1,19 @@
import { immichAdmin, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`immich-admin`, () => {
beforeAll(async () => {
await utils.resetDatabase();
await utils.adminSetup();
});
describe('list-users', () => {
it('should list the admin user', async () => {
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain("email: 'admin@immich.cloud'");
expect(stdout).toContain("name: 'Immich Admin'");
});
});
});

View File

@@ -1,4 +1,5 @@
import {
AllJobStatusResponseDto,
AssetFileUploadResponseDto,
AssetResponseDto,
CreateAlbumDto,
@@ -18,11 +19,14 @@ import {
defaults,
deleteAssets,
getAllAssets,
getAllJobsStatus,
getAssetInfo,
getConfigDefaults,
login,
searchMetadata,
setAdminOnboarding,
signUpAdmin,
updateAdminOnboarding,
updateConfig,
validate,
} from '@immich/sdk';
import { BrowserContext } from '@playwright/test';
@@ -31,6 +35,7 @@ import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path, { dirname } from 'node:path';
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import pg from 'pg';
import { io, type Socket } from 'socket.io-client';
@@ -38,7 +43,7 @@ import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import request from 'supertest';
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
type AdminSetupOptions = { onboarding?: boolean };
@@ -54,13 +59,15 @@ export const testAssetDirInternal = '/data/assets';
export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = async (args: string[]) => {
let _resolve: (value: CliResponse) => void;
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
const child = spawn('node', _args, {
stdio: 'pipe',
});
export const immichCli = (args: string[]) =>
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void;
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
let stdout = '';
let stderr = '';
@@ -133,9 +140,10 @@ export const utils = {
'asset_faces',
'activity',
'api_keys',
'user_token',
'sessions',
'users',
'system_metadata',
'system_config',
];
const sql: string[] = [];
@@ -145,7 +153,12 @@ export const utils = {
}
for (const table of tables) {
sql.push(`DELETE FROM ${table} CASCADE;`);
if (table === 'system_metadata') {
// prevent reverse geocoder from being re-initialized
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
} else {
sql.push(`DELETE FROM ${table} CASCADE;`);
}
}
await client.query(sql.join('\n'));
@@ -209,35 +222,33 @@ export const utils = {
}
},
waitForWebsocketEvent: async ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
if (!id && !count) {
throw new Error('id or count must be provided for waitForWebsocketEvent');
}
const type = id ? `id=${id}` : `count=${count}`;
console.log(`Waiting for ${event} [${type}]`);
const set = events[event];
if ((id && set.has(id)) || (count && set.size >= count)) {
return;
}
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
return new Promise<void>((resolve, reject) => {
if (!id && !count) {
reject(new Error('id or count must be provided for waitForWebsocketEvent'));
}
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
const type = id ? `id=${id}` : `count=${count}`;
console.log(`Waiting for ${event} [${type}]`);
const set = events[event];
const onId = () => {
clearTimeout(timeout);
resolve();
};
if ((id && set.has(id)) || (count && set.size >= count)) {
onId();
return;
}
if (id) {
idCallbacks[id] = () => {
clearTimeout(timeout);
resolve();
};
idCallbacks[id] = onId;
}
if (count) {
countCallbacks[event] = {
count,
callback: () => {
clearTimeout(timeout);
resolve();
},
callback: onId,
};
}
});
@@ -253,7 +264,10 @@ export const utils = {
await signUpAdmin({ signUpDto: signupDto.admin });
const response = await login({ loginCredentialDto: loginDto.admin });
if (options.onboarding) {
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
await updateAdminOnboarding(
{ adminOnboardingUpdateDto: { isOnboarded: true } },
{ headers: asBearerAuth(response.accessToken) },
);
}
return response;
},
@@ -309,9 +323,7 @@ export const utils = {
if (!existsSync(dirname(path))) {
mkdirSync(dirname(path), { recursive: true });
}
if (!existsSync(path)) {
writeFileSync(path, makeRandomImage());
}
writeFileSync(path, makeRandomImage());
},
removeImageFile: (path: string) => {
@@ -406,6 +418,39 @@ export const utils = {
},
]),
resetTempFolder: () => {
rmSync(`${testAssetDir}/temp`, { recursive: true, force: true });
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
},
resetAdminConfig: async (accessToken: string) => {
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
},
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) });
const jobCounts = queues[queue].jobCounts;
return !jobCounts.active && !jobCounts.waiting;
},
waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => {
return new Promise<void>(async (resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
while (true) {
const done = await utils.isQueueEmpty(accessToken, queue);
if (done) {
break;
}
await setAsyncTimeout(200);
}
clearTimeout(timeout);
resolve();
});
},
cliLogin: async (accessToken: string) => {
const key = await utils.createApiKey(accessToken);
await immichCli(['login', app, `${key.secret}`]);

View File

@@ -10,9 +10,9 @@ try {
export default defineConfig({
test: {
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
globalSetup,
testTimeout: 10_000,
testTimeout: 15_000,
poolOptions: {
threads: {
singleThread: true,

View File

@@ -7,7 +7,7 @@
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first.
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment.
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`.
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.

View File

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

View File

@@ -1150,22 +1150,23 @@ test = ["objgraph", "psutil"]
[[package]]
name = "gunicorn"
version = "21.2.0"
version = "22.0.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.7"
files = [
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
]
[package.dependencies]
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]]
@@ -2847,28 +2848,28 @@ files = [
[[package]]
name = "ruff"
version = "0.3.5"
version = "0.3.7"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"},
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"},
{file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"},
{file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"},
{file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"},
{file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
]
[[package]]

View File

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

View File

@@ -1,14 +1,14 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id "kotlin-kapt"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
localPropertiesFile.withInputStream { localProperties.load(it) }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -21,18 +21,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
}
android {
compileSdkVersion 34
@@ -50,7 +44,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 33
@@ -88,6 +81,13 @@ flutter {
}
dependencies {
def kotlin_version = '1.9.23'
def kotlin_coroutines_version = '1.8.0'
def work_version = '2.9.0'
def concurrent_version = '1.1.0'
def guava_version = '33.1.0-android'
def glide_version = '4.16.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"

View File

@@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L
@@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
if (workInfoList != null) {
for (workInfo in workInfoList) {
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
if (workInfo.state == WorkInfo.State.ENQUEUED) {
val workRequest = buildWorkRequest(requireWifi, requireCharging)
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
@@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.setRequiresBatteryNotLow(true)
.setRequiresCharging(requireCharging)
.build();
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
@@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
}
private const val TAG = "BackupWorker"
private const val TAG = "BackupWorker"

View File

@@ -1,21 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.20'
ext.kotlin_coroutines_version = '1.7.1'
ext.work_version = '2.7.1'
ext.concurrent_version = '1.1.0'
ext.guava_version = '33.0.0-android'
ext.glide_version = '4.14.2'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@@ -34,3 +16,7 @@ subprojects {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
tasks.named('wrapper') {
distributionType = Wrapper.DistributionType.ALL
}

View File

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

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000219">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.515419">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="35.431743">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974">
</testcase>

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12

View File

@@ -1,11 +1,26 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
}
include ":app"

View File

@@ -296,6 +296,7 @@
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"no_assets_to_show" : "No assets to show",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
@@ -511,5 +512,7 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack"
}
"viewer_unstack": "Un-Stack",
"haptic_feedback_title": "Haptic Feedback",
"haptic_feedback_switch": "Enable haptic feedback"
}

View File

@@ -296,6 +296,7 @@
"motion_photos_page_title": "Photos avec mouvement",
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
"no_assets_to_show" : "Aucun élément à afficher",
"notification_permission_dialog_cancel": "Annuler",
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
"notification_permission_dialog_settings": "Paramètres",
@@ -509,5 +510,7 @@
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
"viewer_remove_from_stack": "Retirer de la pile",
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
"viewer_unstack": "Désempiler"
"viewer_unstack": "Désempiler",
"haptic_feedback_title": "Retour haptique",
"haptic_feedback_switch": "Activer le retour haptique"
}

Binary file not shown.

View File

@@ -383,7 +383,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -525,7 +525,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -553,7 +553,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 147;
CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.101.0</string>
<string>1.102.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>147</string>
<string>150</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000242">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000231">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.761829">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.155919">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.47461">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.252784">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.179512">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.210502">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="165.636347">
<testcase classname="fastlane.lanes" name="4: build_app" time="175.813647">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="77.651963">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.512517">
</testcase>

View File

@@ -20,6 +20,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
@@ -303,7 +304,9 @@ class GalleryViewerPage extends HookConsumerWidget {
scrollDirection: Axis.horizontal,
onPageChanged: (value) async {
final next = currentIndex.value < value ? value + 1 : value - 1;
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentIndex.value = value;
stackIndex.value = -1;
isPlayingVideo.value = false;

View File

@@ -1,13 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
@@ -21,6 +21,7 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode(
@@ -78,7 +79,7 @@ class AlbumInfoCard extends HookConsumerWidget {
return GestureDetector(
onTap: () {
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
@@ -87,7 +88,7 @@ class AlbumInfoCard extends HookConsumerWidget {
}
},
onDoubleTap: () {
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list

View File

@@ -1,6 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
@@ -68,7 +68,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
return GestureDetector(
onDoubleTap: () {
HapticFeedback.selectionClick();
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list
@@ -93,7 +93,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
class GroupDividerTitle extends HookConsumerWidget {
const GroupDividerTitle({
@@ -38,7 +38,7 @@ class GroupDividerTitle extends HookConsumerWidget {
);
void handleTitleIconClick() {
HapticFeedback.heavyImpact();
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
if (selected) {
onDeselect();
} else {

View File

@@ -6,7 +6,7 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
@@ -15,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart';
@@ -27,7 +28,7 @@ typedef ImmichAssetGridSelectionListener = void Function(
Set<Asset>,
);
class ImmichAssetGridView extends StatefulWidget {
class ImmichAssetGridView extends ConsumerStatefulWidget {
final RenderList renderList;
final int assetsPerRow;
final double margin;
@@ -69,12 +70,12 @@ class ImmichAssetGridView extends StatefulWidget {
});
@override
State<StatefulWidget> createState() {
createState() {
return ImmichAssetGridViewState();
}
}
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
final ItemScrollController _itemScrollController = ItemScrollController();
final ScrollOffsetController _scrollOffsetController =
ScrollOffsetController();
@@ -314,7 +315,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
final now = Timeline.now;
if (now > (_hapticFeedbackTS + feedbackInterval)) {
_hapticFeedbackTS = now;
HapticFeedback.mediumImpact();
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
}
}
}

View File

@@ -1,14 +1,15 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';
import 'package:isar/isar.dart';
class ThumbnailImage extends StatelessWidget {
class ThumbnailImage extends ConsumerWidget {
final Asset asset;
final int index;
final Asset Function(int index) loadAsset;
@@ -37,7 +38,7 @@ class ThumbnailImage extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final assetContainerColor = context.isDarkTheme
? Colors.blueGrey
: context.themeData.primaryColorLight;
@@ -186,7 +187,7 @@ class ThumbnailImage extends StatelessWidget {
},
onLongPress: () {
onSelect?.call();
HapticFeedback.heavyImpact();
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
child: Stack(
children: [

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
@@ -92,7 +91,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
serverUrl: serverUrl,
);
} catch (e) {
HapticFeedback.vibrate();
debugPrint("Error logging in $e");
return false;
}

View File

@@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
class MemoryLane extends HookConsumerWidget {
@@ -33,7 +33,9 @@ class MemoryLane extends HookConsumerWidget {
return GestureDetector(
onTap: () {
HapticFeedback.heavyImpact();
ref
.read(hapticFeedbackProvider.notifier)
.heavyImpact();
context.pushRoute(
MemoryRoute(
memories: memories,

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
@RoutePage()
@@ -127,7 +128,7 @@ class MemoryPage extends HookConsumerWidget {
}
Future<void> onAssetChanged(int otherIndex) async {
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
currentAssetPage.value = otherIndex;
updateProgressText();
// Wait for page change animation to finish
@@ -169,7 +170,7 @@ class MemoryPage extends HookConsumerWidget {
scrollDirection: Axis.vertical,
controller: memoryPageController,
onPageChanged: (pageNumber) {
HapticFeedback.mediumImpact();
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
if (pageNumber < memories.length) {
currentMemoryIndex.value = pageNumber;
currentMemory.value = memories[pageNumber];

View File

@@ -58,6 +58,7 @@ enum AppSettingsEnum<T> {
null,
false,
),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View File

@@ -0,0 +1,81 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
class LanguageSettings extends HookConsumerWidget {
const LanguageSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = context.locale;
final textController = useTextEditingController(
text: locales.keys.firstWhere(
(countryName) => locales[countryName] == currentLocale,
),
);
final selectedLocale = useState<Locale>(currentLocale);
return ListView(
padding: const EdgeInsets.all(16),
children: [
LayoutBuilder(
builder: (context, constraints) {
return DropdownMenu(
width: constraints.maxWidth,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
),
menuStyle: MenuStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
backgroundColor: MaterialStatePropertyAll<Color>(
context.isDarkTheme
? Colors.grey[900]!
: context.scaffoldBackgroundColor,
),
),
menuHeight: context.height * 0.5,
hintText: "Languages",
label: const Text('Languages'),
dropdownMenuEntries: locales.keys
.map(
(countryName) => DropdownMenuEntry(
value: locales[countryName],
label: countryName,
),
)
.toList(),
controller: textController,
onSelected: (value) {
if (value != null) {
selectedLocale.value = value;
}
},
);
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: selectedLocale.value == currentLocale
? null
: () {
context.setLocale(selectedLocale.value);
loadTranslations();
},
child: const Text('setting_languages_apply').tr(),
),
],
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart';
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart';
class HapticSetting extends HookConsumerWidget {
const HapticSetting({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hapticFeedbackSetting =
useAppSettingsState(AppSettingsEnum.enableHapticFeedback);
final isHapticFeedbackEnabled =
useValueNotifier(hapticFeedbackSetting.value);
onHapticFeedbackChange(bool isEnabled) {
hapticFeedbackSetting.value = isEnabled;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "haptic_feedback_title".tr()),
SettingsSwitchListTile(
valueNotifier: isHapticFeedbackEnabled,
title: 'haptic_feedback_switch'.tr(),
onChanged: onHapticFeedbackChange,
),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/settings/ui/preference_settings/haptic_setting.dart';
import 'package:immich_mobile/modules/settings/ui/preference_settings/theme_setting.dart';
import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart';
@@ -11,6 +12,7 @@ class PreferenceSetting extends StatelessWidget {
Widget build(BuildContext context) {
const preferenceSettings = [
ThemeSetting(),
HapticSetting(),
];
return const SettingsSubPageScaffold(settings: preferenceSettings);

View File

@@ -2,14 +2,12 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
import 'package:immich_mobile/modules/settings/ui/advanced_settings.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/language_settings.dart';
import 'package:immich_mobile/modules/settings/ui/notification_setting.dart';
import 'package:immich_mobile/modules/settings/ui/preference_settings/preference_setting.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -42,70 +40,6 @@ enum SettingSection {
const SettingSection(this.title, this.icon);
}
class LanguageSettings extends HookConsumerWidget {
const LanguageSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = context.locale;
final textController = useTextEditingController(
text: locales.keys.firstWhere(
(countryName) => locales[countryName] == currentLocale,
),
);
final selectedLocale = useState<Locale>(currentLocale);
return ListView(
padding: const EdgeInsets.all(16),
children: [
DropdownMenu(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
),
menuStyle: MenuStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
menuHeight: context.height * 0.5,
hintText: "Languages",
label: const Text('Languages'),
dropdownMenuEntries: locales.keys
.map(
(countryName) => DropdownMenuEntry(
value: locales[countryName],
label: countryName,
),
)
.toList(),
controller: textController,
onSelected: (value) {
if (value != null) {
selectedLocale.value = value;
}
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: selectedLocale.value == currentLocale
? null
: () {
context.setLocale(selectedLocale.value);
loadTranslations();
},
child: const Text('setting_languages_apply').tr(),
),
],
);
}
}
@RoutePage()
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});

View File

@@ -191,6 +191,7 @@ enum StoreKey<T> {
selectedAlbumSortReverse<bool>(123, type: bool),
mapThemeMode<int>(124, type: int),
mapwithPartners<bool>(125, type: bool),
enableHapticFeedback<bool>(126, type: bool),
;
const StoreKey(

View File

@@ -0,0 +1,56 @@
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
final hapticFeedbackProvider =
StateNotifierProvider<HapticNotifier, void>((ref) {
return HapticNotifier(ref);
});
class HapticNotifier extends StateNotifier<void> {
void build() {}
final Ref _ref;
HapticNotifier(this._ref) : super(null);
selectionClick() {
if (_ref
.read(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
HapticFeedback.selectionClick();
}
}
lightImpact() {
if (_ref
.read(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
HapticFeedback.lightImpact();
}
}
mediumImpact() {
if (_ref
.read(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
HapticFeedback.mediumImpact();
}
}
heavyImpact() {
if (_ref
.read(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
HapticFeedback.heavyImpact();
}
}
vibrate() {
if (_ref
.read(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.enableHapticFeedback)) {
HapticFeedback.vibrate();
}
}
}

View File

@@ -88,18 +88,19 @@ class ImmichLogger {
Future<void> shareLogs() async {
final tempDir = await getTemporaryDirectory();
final dateTime = DateTime.now().toIso8601String();
final filePath = '${tempDir.path}/Immich_log_$dateTime.csv';
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
final logFile = await File(filePath).create();
final io = logFile.openWrite();
try {
// Write header
io.write("created_at,level,context,message,stacktrace\n");
// Write messages
for (final m in messages) {
io.write(
'${m.createdAt},${m.level},"${m.context1 ?? ""}","${m.message}","${m.context2 ?? ""}"\n',
);
final created = m.createdAt;
final level = m.level.name.padRight(8);
final logger = (m.context1 ?? "<UNKNOWN_LOGGER>").padRight(20);
final message = m.message;
final error = m.details != null ? " ${m.details} |" : "";
final stack = m.context2 != null ? "\n${m.context2!}" : "";
io.write('$created | $level | $logger | $message |$error$stack\n');
}
} finally {
await io.flush();

View File

@@ -39,27 +39,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
);
buildTopRow() {
return Row(
return Stack(
children: [
InkWell(
onTap: () => context.pop(),
child: const Icon(
Icons.close,
size: 20,
Align(
alignment: Alignment.topLeft,
child: InkWell(
onTap: () => context.pop(),
child: const Icon(
Icons.close,
size: 20,
),
),
),
Expanded(
child: Align(
alignment: Alignment.center,
child: Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
color: context.primaryColor,
fontSize: 16,
),
),
Center(
child: Image.asset(
context.isDarkTheme
? 'assets/immich-text-dark.png'
: 'assets/immich-text-light.png',
height: 16,
),
),
],

View File

@@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget {
const Center(child: ImmichLoadingIndicator());
Widget buildEmptyIndicator() =>
emptyIndicator ?? const Center(child: Text("No assets to show"));
emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr());
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -1,13 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/shared/providers/tab.provider.dart';
@RoutePage()
@@ -53,7 +53,7 @@ class TabControllerPage extends HookConsumerWidget {
scrollToTopNotifierProvider.scrollToTop();
}
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
tabsRouter.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
},
@@ -107,7 +107,7 @@ class TabControllerPage extends HookConsumerWidget {
scrollToTopNotifierProvider.scrollToTop();
}
HapticFeedback.selectionClick();
ref.read(hapticFeedbackProvider.notifier).selectionClick();
tabsRouter.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
},

View File

@@ -13,6 +13,7 @@ doc/ActivityCreateDto.md
doc/ActivityResponseDto.md
doc/ActivityStatisticsResponseDto.md
doc/AddUsersDto.md
doc/AdminOnboardingUpdateDto.md
doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
@@ -24,6 +25,7 @@ doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md
doc/AssetDeltaSyncResponseDto.md
doc/AssetFaceResponseDto.md
doc/AssetFaceUpdateDto.md
doc/AssetFaceUpdateItem.md
@@ -40,7 +42,6 @@ doc/AssetTypeEnum.md
doc/AudioCodec.md
doc/AuditApi.md
doc/AuditDeletesResponseDto.md
doc/AuthDeviceResponseDto.md
doc/AuthenticationApi.md
doc/BulkIdResponseDto.md
doc/BulkIdsDto.md
@@ -69,6 +70,7 @@ doc/FaceApi.md
doc/FaceDto.md
doc/FileChecksumDto.md
doc/FileChecksumResponseDto.md
doc/FileReportApi.md
doc/FileReportDto.md
doc/FileReportFixDto.md
doc/FileReportItemDto.md
@@ -122,6 +124,7 @@ doc/QueueStatusDto.md
doc/ReactionLevel.md
doc/ReactionType.md
doc/RecognitionConfig.md
doc/ReverseGeocodingStateResponseDto.md
doc/ScanLibraryDto.md
doc/SearchAlbumResponseDto.md
doc/SearchApi.md
@@ -141,6 +144,8 @@ doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md
doc/ServerThemeDto.md
doc/ServerVersionResponseDto.md
doc/SessionResponseDto.md
doc/SessionsApi.md
doc/SharedLinkApi.md
doc/SharedLinkCreateDto.md
doc/SharedLinkEditDto.md
@@ -149,6 +154,7 @@ doc/SharedLinkType.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
doc/SmartSearchDto.md
doc/SyncApi.md
doc/SystemConfigApi.md
doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md
@@ -170,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThemeDto.md
doc/SystemConfigTrashDto.md
doc/SystemConfigUserDto.md
doc/SystemMetadataApi.md
doc/TagApi.md
doc/TagResponseDto.md
doc/TagTypeEnum.md
@@ -209,6 +216,7 @@ lib/api/audit_api.dart
lib/api/authentication_api.dart
lib/api/download_api.dart
lib/api/face_api.dart
lib/api/file_report_api.dart
lib/api/job_api.dart
lib/api/library_api.dart
lib/api/memory_api.dart
@@ -217,8 +225,11 @@ lib/api/partner_api.dart
lib/api/person_api.dart
lib/api/search_api.dart
lib/api/server_info_api.dart
lib/api/sessions_api.dart
lib/api/shared_link_api.dart
lib/api/sync_api.dart
lib/api/system_config_api.dart
lib/api/system_metadata_api.dart
lib/api/tag_api.dart
lib/api/timeline_api.dart
lib/api/trash_api.dart
@@ -235,6 +246,7 @@ lib/model/activity_create_dto.dart
lib/model/activity_response_dto.dart
lib/model/activity_statistics_response_dto.dart
lib/model/add_users_dto.dart
lib/model/admin_onboarding_update_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart
lib/model/all_job_status_response_dto.dart
@@ -248,6 +260,7 @@ lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart
lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.dart
lib/model/asset_delta_sync_response_dto.dart
lib/model/asset_face_response_dto.dart
lib/model/asset_face_update_dto.dart
lib/model/asset_face_update_item.dart
@@ -263,7 +276,6 @@ lib/model/asset_stats_response_dto.dart
lib/model/asset_type_enum.dart
lib/model/audio_codec.dart
lib/model/audit_deletes_response_dto.dart
lib/model/auth_device_response_dto.dart
lib/model/bulk_id_response_dto.dart
lib/model/bulk_ids_dto.dart
lib/model/change_password_dto.dart
@@ -336,6 +348,7 @@ lib/model/queue_status_dto.dart
lib/model/reaction_level.dart
lib/model/reaction_type.dart
lib/model/recognition_config.dart
lib/model/reverse_geocoding_state_response_dto.dart
lib/model/scan_library_dto.dart
lib/model/search_album_response_dto.dart
lib/model/search_asset_response_dto.dart
@@ -353,6 +366,7 @@ lib/model/server_ping_response.dart
lib/model/server_stats_response_dto.dart
lib/model/server_theme_dto.dart
lib/model/server_version_response_dto.dart
lib/model/session_response_dto.dart
lib/model/shared_link_create_dto.dart
lib/model/shared_link_edit_dto.dart
lib/model/shared_link_response_dto.dart
@@ -411,6 +425,7 @@ test/activity_create_dto_test.dart
test/activity_response_dto_test.dart
test/activity_statistics_response_dto_test.dart
test/add_users_dto_test.dart
test/admin_onboarding_update_dto_test.dart
test/album_api_test.dart
test/album_count_response_dto_test.dart
test/album_response_dto_test.dart
@@ -427,6 +442,7 @@ test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart
test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_test.dart
test/asset_delta_sync_response_dto_test.dart
test/asset_face_response_dto_test.dart
test/asset_face_update_dto_test.dart
test/asset_face_update_item_test.dart
@@ -443,7 +459,6 @@ test/asset_type_enum_test.dart
test/audio_codec_test.dart
test/audit_api_test.dart
test/audit_deletes_response_dto_test.dart
test/auth_device_response_dto_test.dart
test/authentication_api_test.dart
test/bulk_id_response_dto_test.dart
test/bulk_ids_dto_test.dart
@@ -472,6 +487,7 @@ test/face_api_test.dart
test/face_dto_test.dart
test/file_checksum_dto_test.dart
test/file_checksum_response_dto_test.dart
test/file_report_api_test.dart
test/file_report_dto_test.dart
test/file_report_fix_dto_test.dart
test/file_report_item_dto_test.dart
@@ -525,6 +541,7 @@ test/queue_status_dto_test.dart
test/reaction_level_test.dart
test/reaction_type_test.dart
test/recognition_config_test.dart
test/reverse_geocoding_state_response_dto_test.dart
test/scan_library_dto_test.dart
test/search_album_response_dto_test.dart
test/search_api_test.dart
@@ -544,6 +561,8 @@ test/server_ping_response_test.dart
test/server_stats_response_dto_test.dart
test/server_theme_dto_test.dart
test/server_version_response_dto_test.dart
test/session_response_dto_test.dart
test/sessions_api_test.dart
test/shared_link_api_test.dart
test/shared_link_create_dto_test.dart
test/shared_link_edit_dto_test.dart
@@ -552,6 +571,7 @@ test/shared_link_type_test.dart
test/sign_up_dto_test.dart
test/smart_info_response_dto_test.dart
test/smart_search_dto_test.dart
test/sync_api_test.dart
test/system_config_api_test.dart
test/system_config_dto_test.dart
test/system_config_f_fmpeg_dto_test.dart
@@ -573,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart
test/system_config_theme_dto_test.dart
test/system_config_trash_dto_test.dart
test/system_config_user_dto_test.dart
test/system_metadata_api_test.dart
test/tag_api_test.dart
test/tag_response_dto_test.dart
test/tag_type_enum_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.101.0
- API version: 1.102.3
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -112,16 +112,10 @@ Class | Method | HTTP request | Description
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
*AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
*AuthenticationApi* | [**logoutAuthDevice**](doc//AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
@@ -129,6 +123,9 @@ Class | Method | HTTP request | Description
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face |
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /report/fix |
*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /report |
*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /report/checksum |
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
@@ -182,7 +179,9 @@ Class | Method | HTTP request | Description
*ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
@@ -191,11 +190,16 @@ Class | Method | HTTP request | Description
*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
*SyncApi* | [**getAllForUserFullSync**](doc//SyncApi.md#getallforuserfullsync) | **GET** /sync/full-sync |
*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **GET** /sync/delta-sync |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
*SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json |
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |
*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state |
*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding |
*TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag |
*TagApi* | [**deleteTag**](doc//TagApi.md#deletetag) | **DELETE** /tag/{id} |
*TagApi* | [**getAllTags**](doc//TagApi.md#getalltags) | **GET** /tag |
@@ -231,6 +235,7 @@ Class | Method | HTTP request | Description
- [ActivityResponseDto](doc//ActivityResponseDto.md)
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
@@ -240,6 +245,7 @@ Class | Method | HTTP request | Description
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
@@ -255,7 +261,6 @@ Class | Method | HTTP request | Description
- [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md)
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
- [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md)
- [CLIPConfig](doc//CLIPConfig.md)
@@ -328,6 +333,7 @@ Class | Method | HTTP request | Description
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
- [RecognitionConfig](doc//RecognitionConfig.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
- [ScanLibraryDto](doc//ScanLibraryDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
@@ -345,6 +351,7 @@ Class | Method | HTTP request | Description
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerThemeDto](doc//ServerThemeDto.md)
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)

View File

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

View File

@@ -0,0 +1,17 @@
# openapi.model.AssetDeltaSyncResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**deleted** | **List<String>** | | [default to const []]
**needsFullSync** | **bool** | |
**upserted** | [**List<AssetResponseDto>**](AssetResponseDto.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

@@ -9,66 +9,9 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
[**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report |
[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
# **fixAuditFiles**
> fixAuditFiles(fileReportFixDto)
### 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 = AuditApi();
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
try {
api_instance.fixAuditFiles(fileReportFixDto);
} catch (e) {
print('Exception when calling AuditApi->fixAuditFiles: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditDeletes**
> AuditDeletesResponseDto getAuditDeletes(after, entityType, userId)
@@ -128,109 +71,3 @@ 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)
# **getAuditFiles**
> FileReportDto getAuditFiles()
### 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 = AuditApi();
try {
final result = api_instance.getAuditFiles();
print(result);
} catch (e) {
print('Exception when calling AuditApi->getAuditFiles: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**FileReportDto**](FileReportDto.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)
# **getFileChecksums**
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
### 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 = AuditApi();
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
try {
final result = api_instance.getFileChecksums(fileChecksumDto);
print(result);
} catch (e) {
print('Exception when calling AuditApi->getFileChecksums: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
### Return type
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.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

@@ -10,11 +10,8 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
[**login**](AuthenticationApi.md#login) | **POST** /auth/login |
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
[**logoutAuthDevice**](AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
[**logoutAuthDevices**](AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
[**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
@@ -74,57 +71,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)
# **getAuthDevices**
> List<AuthDeviceResponseDto> getAuthDevices()
### 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 = AuthenticationApi();
try {
final result = api_instance.getAuthDevices();
print(result);
} catch (e) {
print('Exception when calling AuthenticationApi->getAuthDevices: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**List<AuthDeviceResponseDto>**](AuthDeviceResponseDto.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)
# **login**
> LoginResponseDto login(loginCredentialDto)
@@ -217,110 +163,6 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **logoutAuthDevice**
> logoutAuthDevice(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 = AuthenticationApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.logoutAuthDevice(id);
} catch (e) {
print('Exception when calling AuthenticationApi->logoutAuthDevice: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **logoutAuthDevices**
> logoutAuthDevices()
### 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 = AuthenticationApi();
try {
api_instance.logoutAuthDevices();
} catch (e) {
print('Exception when calling AuthenticationApi->logoutAuthDevices: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **signUpAdmin**
> UserResponseDto signUpAdmin(signUpDto)

176
mobile/openapi/doc/FileReportApi.md generated Normal file
View File

@@ -0,0 +1,176 @@
# openapi.api.FileReportApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**fixAuditFiles**](FileReportApi.md#fixauditfiles) | **POST** /report/fix |
[**getAuditFiles**](FileReportApi.md#getauditfiles) | **GET** /report |
[**getFileChecksums**](FileReportApi.md#getfilechecksums) | **POST** /report/checksum |
# **fixAuditFiles**
> fixAuditFiles(fileReportFixDto)
### 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 = FileReportApi();
final fileReportFixDto = FileReportFixDto(); // FileReportFixDto |
try {
api_instance.fixAuditFiles(fileReportFixDto);
} catch (e) {
print('Exception when calling FileReportApi->fixAuditFiles: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuditFiles**
> FileReportDto getAuditFiles()
### 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 = FileReportApi();
try {
final result = api_instance.getAuditFiles();
print(result);
} catch (e) {
print('Exception when calling FileReportApi->getAuditFiles: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**FileReportDto**](FileReportDto.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)
# **getFileChecksums**
> List<FileChecksumResponseDto> getFileChecksums(fileChecksumDto)
### 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 = FileReportApi();
final fileChecksumDto = FileChecksumDto(); // FileChecksumDto |
try {
final result = api_instance.getFileChecksums(fileChecksumDto);
print(result);
} catch (e) {
print('Exception when calling FileReportApi->getFileChecksums: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| |
### Return type
[**List<FileChecksumResponseDto>**](FileChecksumResponseDto.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

@@ -0,0 +1,16 @@
# openapi.model.ReverseGeocodingStateResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**lastImportFileName** | **String** | |
**lastUpdate** | **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

@@ -17,7 +17,6 @@ Method | HTTP request | Description
[**getSupportedMediaTypes**](ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
[**getTheme**](ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
[**setAdminOnboarding**](ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
# **getServerConfig**
@@ -344,53 +343,3 @@ No authorization required
[[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)
# **setAdminOnboarding**
> setAdminOnboarding()
### 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 = ServerInfoApi();
try {
api_instance.setAdminOnboarding();
} catch (e) {
print('Exception when calling ServerInfoApi->setAdminOnboarding: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -1,4 +1,4 @@
# openapi.model.AuthDeviceResponseDto
# openapi.model.SessionResponseDto
## Load the model package
```dart

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