Compare commits

...

32 Commits

Author SHA1 Message Date
Alex Tran
3760782d7f refactor combat message 2024-08-13 16:34:04 -05:00
Weblate (bot)
a8a63b24d0 chore(web): update translations (#11533)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: AMT AMT <altmimiamt@gmail.com>
Co-authored-by: Adam Uchmanowicz <auchmanowicz@gmail.com>
Co-authored-by: António Santos <antoniomsantos99@gmail.com>
Co-authored-by: Atakan Dulker <atakandulker@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: Cristian Florin Tănase <crissssty@gmail.com>
Co-authored-by: Czerjak N <czerjaknorbert@gmail.com>
Co-authored-by: Dmitry <kittyfriend@mail.ru>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: ElTopo <cameos@gmail.com>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Felipe Silva <dorsal-cobweb-life@duck.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Furkan Yutup <furkanyutupre@gmail.com>
Co-authored-by: Hugo Cossard <hugococa2004@gmail.com>
Co-authored-by: Ionut <ionutp626@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Lars Bernstein <lb@setq.de>
Co-authored-by: Laurentiu <laurfb@gmail.com>
Co-authored-by: Lauritz Tieste <lauritz6000000@gmail.com>
Co-authored-by: Luna Kowalik <0skar16.contact@gmail.com>
Co-authored-by: MM <metalmario90@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Oliver Larsson <larsson.e.oliver@gmail.com>
Co-authored-by: Peder Kollenborg <pederkollenborg@gmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Pruthvi Bugidi <bps.21@proton.me>
Co-authored-by: Riccardo <lark-unit-rush@duck.com>
Co-authored-by: Rosu Iulian <rosuiulian@gmail.com>
Co-authored-by: Rıfat Dinç <rafidinc41@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: Simon Zeeck Svärd <simon.svard100@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: TheScientistPT <joao.ed.reis.gomes@gmail.com>
Co-authored-by: Tobias Frejo <tobiasfrejo@gmail.com>
Co-authored-by: Tom Niget <zippedfire@free.fr>
Co-authored-by: UTKARSH VISHNOI <utkarshvishnoi25@gmail.com>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Vincent Yeung <yeung_pok_yin_405060@yahoo.com.hk>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: Voinea Laurentiu Gabriel <gabivoinea29@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: anton <reallygud@protonmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dkorecko <reset259@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jocxfin <joonatan@joonatanh.com>
Co-authored-by: manosrh <manosrh@gmail.com>
Co-authored-by: oopzzozzo <ek3ru8m4@gmail.com>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: sibber5 <ghasjado@gmail.com>
Co-authored-by: thestrudl <rok.vidmar1997@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Åke Amcoff <ake@amcoff.net>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-08-13 20:48:17 +00:00
Jason Rasmussen
ab0ed11778 chore: separate enhancement group in release notes (#11756) 2024-08-13 16:39:25 -04:00
Alex
5ec407b57c chore(mobile): properly patch openapi with custom response dto (#11753) 2024-08-13 14:39:25 -05:00
martin
fdf0b16fe3 feat(web): add privacy step in the onboarding (#11359)
* feat: add privacy step in the onboarding

* fix: remove console.log

* feat:Details the implications of enabling the map on the settings page

Added a link to the guide on customizing map styles as well

* feat: add map implication

* refactor: onboarding style

* fix: tile provider

* fix: remove long explanations

* chore: cleanup

---------

Co-authored-by: pcouy <contact@pierre-couy.dev>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 17:01:30 +00:00
Pierre Couy
c924f6c27c docs: update custom map style guide (#11350)
* docs:Reword "Custom Map Style" guide

- Split setting a style.json in Immich and creating a style with
  Maptiler
- Make it clearer that this is the way to change tile provider

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 16:05:36 +00:00
Carsten Otto
df45ef0e35 fix(server): follow symlinks when zipping assets (#11685)
* follow symlinks when zipping assets

fixes #9335

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 11:39:24 -04:00
renovate[bot]
81c813a882 chore(deps): update dependency tailwindcss to v3.4.9 (#11750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 11:37:06 -04:00
Michel Heusschen
b014162088 refactor(web): add tailwind plugin for repeating grid cols (#11748) 2024-08-13 11:36:46 -04:00
Michel Heusschen
276101ee82 feat(web): improve shared link management on mobile (#11720)
* feat(web): improve shared link management on mobile

* fix format
2024-08-13 09:37:47 -05:00
renovate[bot]
9837d60074 chore(deps): update dependency vite-tsconfig-paths to v5 (#11746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 08:40:22 -04:00
renovate[bot]
28b7443b92 chore(deps): update base-image to v20240813 (major) (#11747)
chore(deps): update base-image to v20240813

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 12:26:22 +00:00
Michel Heusschen
5acdc958b6 fix(web): single row of items (#11729)
* fix(web): single row of items

* remove filterBoxWidth

* slight size adjustment

* rewrite action as component
2024-08-13 08:20:08 -04:00
renovate[bot]
e384692025 chore(deps): update typescript-projects (#11743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 08:17:17 -04:00
renovate[bot]
54b276c984 chore(deps): update dependency @types/node to ^20.14.14 (#11737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 23:31:57 -04:00
Jason Rasmussen
7eb004bd00 chore: better release notes (#11726)
* chore: better release notes

* chore: remove 'tedious' commits
2024-08-12 14:49:07 -04:00
Michel Heusschen
c2965c4408 fix(web): detail panel out of sync when reopening (#11713)
* fix(web): detail panel out of sync when reopening

* extract event handler
2024-08-12 08:10:43 -04:00
Michel Heusschen
b749a68349 fix(web): hide import json button when using config file (#11714) 2024-08-12 07:40:31 -04:00
Michel Heusschen
30aa2c9b82 fix(web): use fallback image if shared asset isn't resized (#11704)
* fix(web): use fallback image if shared asset isn't resized

* remove test-data index file
2024-08-11 15:43:07 -04:00
Robert Schütz
9ed04588b8 chore(deps): update pydantic to v2 (#11701) 2024-08-11 12:23:11 -04:00
Michel Heusschen
7d320217b9 chore(web): remove unused file (#11696) 2024-08-11 08:01:37 -04:00
Michel Heusschen
efdf8bbca9 refactor(web): simplify some stores (#11695)
* refactor(web): simplify some stores

* make writable
2024-08-11 08:01:16 -04:00
Michel Heusschen
34c4fbf730 fix(web): asset viewer dynamic size (#11697) 2024-08-11 07:59:26 -04:00
Matthew Momjian
ca775ab3e9 docs: Update docs + example.env for DB_PASSWORD (#11678) 2024-08-09 21:36:32 +00:00
renovate[bot]
2dd5514043 chore(deps): update prom/prometheus docker digest to cafe963 (#11673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-09 14:07:25 -04:00
Christoph Suter
f33dbdfe9a feat(web): add Exif-Rating (#11580)
* Add Exif-Rating

* Integrate star rating as own component

* Add e2e tests for rating and validation

* Rename component and async handleChangeRating

* Display rating can be enabled in app settings

* Correct i18n reference

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Star rating: change from slider to buttons

* Star rating for clarity

* Design updates.

* Renaming and code optimization

* chore: clean up

* chore: e2e formatting

* light mode border and default value

---------

Co-authored-by: Christoph Suter <christoph@suter-burri.ch>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-09 17:45:52 +00:00
Saschl
b1587a5dee feat(mobile): darken screen on backup page (#11623)
* feat: keep screen active on backup

* show dialog

* improve dialog and use shared timer

* get rid of confirmation dialog

* fix timer logic

* fix: set timeout to 60 seconds

* fix: revert unwanted change

* fix: properly hide status bar

* remove unwanted change

* fix: properly restore status bar when waking up

* clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-09 16:39:33 +00:00
Alex
501485d0b1 fix(mobile): incorrect remove action from the album assets detail view (#11671)
* fix(mobile): incorrect remove action from the album assets detail view

* better data structure
2024-08-09 09:51:08 -05:00
renovate[bot]
ed7f857975 chore(deps): update prom/prometheus docker digest to 497fe92 (#11669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-09 10:00:50 -04:00
Alex
d346985457 chore(mobile): refactor detail panel (#11662)
* date time component

* rename to info_sheet

* simplify map info

* Edit datetime sheet

* fix janking when scroll on info sheet

* Location refactor

* refactor name

* Update date time after editing

* localize rebuild to smaller component

* restore advanced bottom sheet

* reassign EXIF back to local database

* remove print statements
2024-08-09 13:43:47 +00:00
bo0tzz
a144a1bec3 chore: add warning to media location env var (#11665) 2024-08-09 07:29:55 -04:00
Carsten Otto
9f318a9338 fix(docs): update documentation (#11655)
update documentation
2024-08-08 23:03:43 +00:00
180 changed files with 6777 additions and 3551 deletions

36
.github/release.yml vendored
View File

@@ -1,41 +1,29 @@
changelog:
categories:
- title: ⚠️ Breaking Changes
- title: 🚨 Breaking Changes
labels:
- breaking-change
- title: 🗄️ Server
- title: 🔒 Security
labels:
- 🗄server
- security
- title: 📱 Mobile
- title: 🚀 Features
labels:
- 📱mobile
- feature
- title: 🖥️ Web
- title: 🌟 Enhancements
labels:
- 🖥web
- enhancement
- title: 🧠 Machine Learning
- title: 🐛 Bug fixes
labels:
- 🧠machine-learning
- bugfix
- title: ⚡ CLI
labels:
- cli
- title: 📓 Documentation
- title: 📚 Documentation
labels:
- documentation
- title: 🔨 Maintenance
- title: 🌐 Translations
labels:
- deployment
- dependencies
- renovate
- maintenance
- tech-debt
- title: Other changes
labels:
- "*"
- translation

37
cli/package-lock.json generated
View File

@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -41,7 +41,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5",
"vitest-fetch-mock": "^0.3.0",
"yaml": "^2.3.1"
@@ -59,7 +59,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"typescript": "^5.3.3"
}
},
@@ -1269,9 +1269,9 @@
}
},
"node_modules/@types/node": {
"version": "20.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
"integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3427,9 +3427,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"dev": true,
"funding": [
{
@@ -4206,14 +4206,14 @@
}
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"rollup": "^4.13.0"
},
"bin": {
@@ -4233,6 +4233,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -4250,6 +4251,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
@@ -4284,10 +4288,11 @@
}
},
"node_modules/vite-tsconfig-paths": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
"integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",

View File

@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -37,7 +37,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5",
"vitest-fetch-mock": "^0.3.0",
"yaml": "^2.3.1"

View File

@@ -79,7 +79,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
image: prom/prometheus@sha256:cafe963e591c872d38f3ea41ff8eb22cee97917b7c97b5c0ccd43a419f11f613
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus

View File

@@ -12,6 +12,7 @@ DB_DATA_LOCATION=./postgres
IMMICH_VERSION=release
# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=postgres
# The values below this line do not need to be changed

View File

@@ -4,7 +4,8 @@
### Unit tests
Unit are run by calling `npm run test` from the `server` directory.
Unit are run by calling `npm run test` from the `server/` directory.
You need to run `npm install` (in `server/`) before _once_.
### End to end tests
@@ -14,6 +15,11 @@ The e2e tests can be run by first starting up a test production environment via:
make e2e
```
Before you can run the tests, you need to run the following commands _once_:
- `npm install` (in `e2e/`)
- `make open-api` (in the project root `/`)
Once the test environment is running, the e2e tests can be run via:
```bash

View File

@@ -1,8 +1,22 @@
# Create Custom Map Styles for Immich Using Maptiler
# Custom Map Styles
You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
You may decide that you'd like to modify the style document which is used to
draw the maps in Immich. In addition to visual customization, this also allows
you to pick your own map tile provider instead of the default one. The default
`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json)
and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json)
can be used as a basis for creating your own style.
## Steps
There are several sources for already-made `style.json` map themes, as well as
online generators you can use.
1. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style!
## Use Maptiler to build a custom style
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
@@ -11,6 +25,3 @@ You may decide that you'd like to modify the style document which is used to dra
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.png)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.
10. Save your selections. Reload the map, and enjoy your custom map style!

View File

@@ -56,7 +56,8 @@ Optionally, you can enable hardware acceleration for machine learning and transc
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Consider changing `DB_PASSWORD` to something randomly generated
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`.
### Step 3 - Start the containers

View File

@@ -38,21 +38,23 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :-------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*1</sup>⚠️ | `./upload`<sup>\*2</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
:::tip

68
docs/package-lock.json generated
View File

@@ -4237,9 +4237,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"funding": [
{
"type": "opencollective",
@@ -4254,12 +4254,13 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -4531,9 +4532,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"funding": [
{
"type": "opencollective",
@@ -4548,11 +4549,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -4699,9 +4701,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001614",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz",
"integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==",
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"funding": [
{
"type": "opencollective",
@@ -4715,7 +4717,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
@@ -6342,9 +6345,10 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.751",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz",
"integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw=="
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
"integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -11958,9 +11962,10 @@
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"license": "MIT"
},
"node_modules/nopt": {
"version": "1.0.10",
@@ -16015,9 +16020,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -16608,9 +16613,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"funding": [
{
"type": "opencollective",
@@ -16625,9 +16630,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"

41
e2e/package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -99,7 +99,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"typescript": "^5.3.3"
}
},
@@ -1113,13 +1113,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz",
"integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz",
"integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.45.3"
"playwright": "1.46.0"
},
"bin": {
"playwright": "cli.js"
@@ -1516,9 +1516,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
"integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4357,10 +4357,11 @@
"dev": true
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
@@ -5177,13 +5178,13 @@
}
},
"node_modules/playwright": {
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz",
"integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
"integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.45.3"
"playwright-core": "1.46.0"
},
"bin": {
"playwright": "cli.js"
@@ -5196,9 +5197,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz",
"integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz",
"integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",

View File

@@ -43,6 +43,7 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -72,6 +73,7 @@ describe('/asset', () => {
let user2Assets: AssetMediaResponseDto[];
let stackAssets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto;
let ratingAsset: AssetMediaResponseDto;
const setupTests = async () => {
await utils.resetDatabase();
@@ -99,6 +101,16 @@ describe('/asset', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id });
// asset rating
ratingAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'mongolels.jpg',
bytes: await readFile(ratingAssetFilepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: ratingAsset.id });
user1Assets = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
@@ -214,6 +226,22 @@ describe('/asset', () => {
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('should get the asset rating', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: ratingAsset.id,
});
const { status, body } = await request(app)
.get(`/assets/${ratingAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: ratingAsset.id,
exifInfo: expect.objectContaining({ rating: 3 }),
});
});
it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
@@ -575,6 +603,31 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it('should set the rating', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ rating: 2 });
expect(body).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({
rating: 2,
}),
});
expect(status).toEqual(200);
});
it('should reject invalid rating', async () => {
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should return tagged people', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)

View File

@@ -1,16 +1,23 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import type { Socket } from 'socket.io-client';
import { utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
let websocket: Socket;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
websocket = await utils.connectWebsocket(admin.accessToken);
});
test.afterAll(() => {
utils.disconnectWebsocket(websocket);
});
test('can be opened for shared links', async ({ page }) => {
@@ -57,4 +64,23 @@ test.describe('Detail Panel', () => {
await expect(textarea).toBeVisible();
await expect(textarea).not.toBeDisabled();
});
test('description changes are visible after reopening', async ({ context, page }) => {
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await page.getByRole('button', { name: 'Info' }).click();
const textarea = page.getByRole('textbox', { name: 'Add a description' });
await textarea.fill('new description');
await expect(textarea).toHaveValue('new description');
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).not.toBeVisible();
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).toBeVisible();
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
await expect(textarea).toHaveValue('new description');
});
});

View File

@@ -33,6 +33,7 @@ test.describe('Registration', () => {
// onboarding
await expect(page).toHaveURL('/auth/onboarding');
await page.getByRole('button', { name: 'Theme' }).click();
await page.getByRole('button', { name: 'Privacy' }).click();
await page.getByRole('button', { name: 'Storage Template' }).click();
await page.getByRole('button', { name: 'Done' }).click();

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from socket import socket
from gunicorn.arbiter import Arbiter
from pydantic import BaseModel, BaseSettings
from pydantic.v1 import BaseModel, BaseSettings
from rich.console import Console
from rich.logging import RichHandler
from uvicorn import Server

View File

@@ -15,7 +15,7 @@ from fastapi import Depends, FastAPI, File, Form, HTTPException
from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image
from pydantic import ValidationError
from pydantic.v1 import ValidationError
from starlette.formparsers import MultiPartParser
from app.models import get_model_deps

View File

@@ -3,7 +3,7 @@ from typing import Any, Literal, Protocol, TypedDict, TypeGuard, TypeVar
import numpy as np
import numpy.typing as npt
from pydantic import BaseModel
from pydantic.v1 import BaseModel
class StrEnum(str, Enum):

View File

@@ -40,6 +40,17 @@ develop = ["imgaug (>=0.4.0)", "pytest"]
imgaug = ["imgaug (>=0.4.0)"]
tests = ["pytest"]
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.2.0"
@@ -1551,8 +1562,8 @@ psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
pyzmq = ">=25.0.0"
requests = [
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
]
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""}
@@ -2074,10 +2085,10 @@ files = [
[package.dependencies]
numpy = [
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
]
[[package]]
@@ -2117,6 +2128,8 @@ files = [
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
@@ -2367,62 +2380,126 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.17"
description = "Data validation and settings management using python type hints"
version = "2.8.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
{file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
{file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
{file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
{file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
{file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
{file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
{file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
{file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
{file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
{file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
]
[package.dependencies]
typing-extensions = ">=4.2.0"
annotated-types = ">=0.4.0"
pydantic-core = "2.20.1"
typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
]
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.20.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
{file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
{file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
{file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
{file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
{file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
{file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
{file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
{file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
{file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
{file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
{file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
{file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
{file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pygments"
@@ -3244,6 +3321,17 @@ files = [
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "urllib3"
version = "2.1.0"
@@ -3600,4 +3688,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab"
content-hash = "187485f19267f2d0a01e38fc0c1f8911c07a29aee11080179a96a127abb9c11b"

View File

@@ -13,7 +13,7 @@ opencv-python-headless = ">=4.7.0.72,<5.0"
pillow = ">=9.5.0,<11.0"
fastapi-slim = ">=0.95.2,<1.0"
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
pydantic = "^1.10.8"
pydantic = "^2.8.2"
aiocache = ">=0.12.1,<1.0"
rich = ">=13.4.2"
ftfy = ">=6.1.1"

View File

@@ -201,7 +201,7 @@
"delete_shared_link_dialog_title": "Delete Shared Link",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_date_time": "Edit date and time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Location",
"exif_bottom_sheet_description": "Add Description...",

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -170,6 +170,30 @@ class ExifInfo {
state.hashCode ^
country.hashCode ^
description.hashCode;
@override
String toString() {
return """
{
id: $id,
fileSize: $fileSize,
dateTimeOriginal: $dateTimeOriginal,
timeZone: $timeZone,
make: $make,
model: $model,
lens: $lens,
f: $f,
mm: $mm,
iso: $iso,
exposureSeconds: $exposureSeconds,
lat: $lat,
long: $long,
city: $city,
state: $state,
country: $country,
description: $description,
}""";
}
}
double? _exposureTimeToSeconds(String? s) {

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -29,6 +31,9 @@ class BackupControllerPage extends HookConsumerWidget {
BackUpState backupState = ref.watch(backupProvider);
final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
final didGetBackupInfo = useState(false);
final isScreenDarkened = useState(false);
final darkenScreenTimer = useRef<Timer?>(null);
bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground;
bool shouldBackup = backupState.allUniqueAssets.length -
@@ -38,6 +43,25 @@ class BackupControllerPage extends HookConsumerWidget {
? false
: true;
void startScreenDarkenTimer() {
darkenScreenTimer.value = Timer(const Duration(seconds: 30), () {
isScreenDarkened.value = true;
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
});
}
void stopScreenDarkenTimer() {
isScreenDarkened.value = false;
darkenScreenTimer.value?.cancel();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [
SystemUiOverlay.top,
SystemUiOverlay.bottom,
],
);
}
useEffect(
() {
// Update the background settings information just to make sure we
@@ -52,8 +76,11 @@ class BackupControllerPage extends HookConsumerWidget {
.stopListenToEvent('on_upload_success');
WakelockPlus.enable();
return () {
WakelockPlus.disable();
darkenScreenTimer.value?.cancel();
isScreenDarkened.value = false;
};
},
[],
@@ -71,6 +98,19 @@ class BackupControllerPage extends HookConsumerWidget {
[backupState.backupProgress],
);
useEffect(
() {
if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
startScreenDarkenTimer();
} else {
stopScreenDarkenTimer();
}
return null;
},
[backupState.backupProgress],
);
Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums;
@@ -257,72 +297,102 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
return Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text(
"backup_controller_page_backup",
).tr(),
leading: IconButton(
onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent();
context.maybePop(true);
},
splashRadius: 24,
icon: const Icon(
Icons.arrow_back_ios_rounded,
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
onPressed: () => context.pushRoute(const BackupOptionsRoute()),
return GestureDetector(
onTap: () {
if (isScreenDarkened.value) {
stopScreenDarkenTimer();
}
if (backupState.backupProgress == BackUpProgressEnum.inProgress) {
startScreenDarkenTimer();
}
},
child: AnimatedOpacity(
opacity: isScreenDarkened.value ? 0.1 : 1.0,
duration: const Duration(seconds: 1),
child: Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text(
"backup_controller_page_backup",
).tr(),
leading: IconButton(
onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent();
context.maybePop(true);
},
splashRadius: 24,
icon: const Icon(
Icons.settings_outlined,
Icons.arrow_back_ios_rounded,
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
onPressed: () =>
context.pushRoute(const BackupOptionsRoute()),
splashRadius: 24,
icon: const Icon(
Icons.settings_outlined,
),
),
),
],
),
body: Stack(
children: [
Padding(
padding:
const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
child: ListView(
// crossAxisAlignment: CrossAxisAlignment.start,
children: hasAnyAlbum
? [
buildFolderSelectionTile(),
BackupInfoCard(
title: "backup_controller_page_total".tr(),
subtitle: "backup_controller_page_total_sub".tr(),
info: ref
.watch(backupProvider)
.availableAlbums
.isEmpty
? "..."
: "${backupState.allUniqueAssets.length}",
),
BackupInfoCard(
title: "backup_controller_page_backup".tr(),
subtitle: "backup_controller_page_backup_sub".tr(),
info: ref
.watch(backupProvider)
.availableAlbums
.isEmpty
? "..."
: "${backupState.selectedAlbumsBackupAssetsIds.length}",
),
BackupInfoCard(
title: "backup_controller_page_remainder".tr(),
subtitle:
"backup_controller_page_remainder_sub".tr(),
info: ref
.watch(backupProvider)
.availableAlbums
.isEmpty
? "..."
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
),
const Divider(),
const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(),
]
: [
buildFolderSelectionTile(),
if (!didGetBackupInfo.value) buildLoadingIndicator(),
],
),
),
],
),
],
),
body: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
child: ListView(
// crossAxisAlignment: CrossAxisAlignment.start,
children: hasAnyAlbum
? [
buildFolderSelectionTile(),
BackupInfoCard(
title: "backup_controller_page_total".tr(),
subtitle: "backup_controller_page_total_sub".tr(),
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${backupState.allUniqueAssets.length}",
),
BackupInfoCard(
title: "backup_controller_page_backup".tr(),
subtitle: "backup_controller_page_backup_sub".tr(),
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${backupState.selectedAlbumsBackupAssetsIds.length}",
),
BackupInfoCard(
title: "backup_controller_page_remainder".tr(),
subtitle: "backup_controller_page_remainder_sub".tr(),
info: ref.watch(backupProvider).availableAlbums.isEmpty
? "..."
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
),
const Divider(),
const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(),
]
: [
buildFolderSelectionTile(),
if (!didGetBackupInfo.value) buildLoadingIndicator(),
],
),
),
);

View File

@@ -22,7 +22,7 @@ import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart';
import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
@@ -152,7 +152,7 @@ class GalleryViewerPage extends HookConsumerWidget {
.watch(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
? AdvancedBottomSheet(assetDetail: asset)
: ExifBottomSheet(asset: asset),
: DetailPanel(asset: asset),
),
);
},

View File

@@ -234,14 +234,6 @@ class SharedLinkEditPage extends HookConsumerWidget {
onSelected: (value) {
expiryAfter.value = value!;
},
inputDecorationTheme: themeData.inputDecorationTheme.copyWith(
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
),
dropdownMenuEntries: [
DropdownMenuEntry(
value: 0,

View File

@@ -162,6 +162,7 @@ class AssetService {
final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!);
if (dto != null && dto.exifInfo != null) {
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
a.exifInfo = newExif;
if (newExif != a.exifInfo) {
if (a.isInDb) {
_db.writeTxn(() => a.put(_db));

View File

@@ -43,6 +43,19 @@ class AssetDescriptionService {
}
}
}
String getAssetDescription(Asset asset) {
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {
return "";
}
final exifInfo = _db.exifInfos.getSync(localExifId);
return exifInfo?.description ?? "";
}
}
final assetDescriptionServiceProvider = Provider(

View File

@@ -251,11 +251,13 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
borderSide: BorderSide(
color: primaryColor,
),
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: colorScheme.outlineVariant,
),
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
labelStyle: TextStyle(
color: primaryColor,
@@ -268,5 +270,34 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
textSelectionTheme: TextSelectionThemeData(
cursorColor: primaryColor,
),
dropdownMenuTheme: DropdownMenuThemeData(
menuStyle: MenuStyle(
shape: WidgetStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
inputDecorationTheme: InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: primaryColor,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: colorScheme.outlineVariant,
),
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
labelStyle: TextStyle(
color: primaryColor,
),
hintStyle: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
),
);
}

View File

@@ -0,0 +1,12 @@
import 'package:openapi/api.dart';
dynamic upgradeDto(dynamic value, String targetType) {
switch (targetType) {
case 'UserPreferencesResponseDto':
if (value is Map) {
if (value['rating'] == null) {
value['rating'] = RatingResponse().toJson();
}
}
}
}

View File

@@ -118,6 +118,7 @@ Future<void> handleEditDateTime(
initialTZ: timeZone,
initialTZOffset: offset,
);
if (dateTime == null) {
return;
}
@@ -142,10 +143,12 @@ Future<void> handleEditLocation(
);
}
}
final location = await showLocationPicker(
context: context,
initialLatLng: initialLatLng,
);
if (location == null) {
return;
}

View File

@@ -61,58 +61,6 @@ class BottomGalleryBar extends ConsumerWidget {
navStack.length > 2 &&
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false;
// !!!! itemsList and actionlist should always be in sync
final itemsList = [
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
if (asset.isImage)
BottomNavigationBarItem(
icon: const Icon(Icons.tune_outlined),
label: 'control_bottom_app_bar_edit'.tr(),
tooltip: 'control_bottom_app_bar_edit'.tr(),
),
if (isOwner)
asset.isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
),
if (isOwner && stack.isNotEmpty)
BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined),
label: 'control_bottom_app_bar_stack'.tr(),
tooltip: 'control_bottom_app_bar_stack'.tr(),
),
if (isOwner && !isInAlbum)
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
if (!isOwner)
BottomNavigationBarItem(
icon: const Icon(Icons.download_outlined),
label: 'download'.tr(),
tooltip: 'download'.tr(),
),
if (isInAlbum)
BottomNavigationBarItem(
icon: const Icon(Icons.remove_circle_outline),
label: 'album_viewer_appbar_share_remove'.tr(),
tooltip: 'album_viewer_appbar_share_remove'.tr(),
),
];
void removeAssetFromStack() {
if (stackIndex > 0 && showStack) {
@@ -366,16 +314,71 @@ class BottomGalleryBar extends ConsumerWidget {
}
}
List<Function(int)> actionslist = [
(_) => shareAsset(),
if (asset.isImage) (_) => handleEdit(),
if (isOwner) (_) => handleArchive(),
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
if (isOwner) (_) => handleDelete(),
if (!isOwner) (_) => handleDownload(),
if (isInAlbum) (_) => handleRemoveFromAlbum(),
final List<Map<BottomNavigationBarItem, Function(int)>> albumActions = [
{
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
): (_) => shareAsset(),
},
if (asset.isImage)
{
BottomNavigationBarItem(
icon: const Icon(Icons.tune_outlined),
label: 'control_bottom_app_bar_edit'.tr(),
tooltip: 'control_bottom_app_bar_edit'.tr(),
): (_) => handleEdit(),
},
if (isOwner)
{
asset.isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
): (_) => handleArchive(),
},
if (isOwner && stack.isNotEmpty)
{
BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined),
label: 'control_bottom_app_bar_stack'.tr(),
tooltip: 'control_bottom_app_bar_stack'.tr(),
): (_) => showStackActionItems(),
},
if (isOwner && !isInAlbum)
{
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
): (_) => handleDelete(),
},
if (!isOwner)
{
BottomNavigationBarItem(
icon: const Icon(Icons.download_outlined),
label: 'download'.tr(),
tooltip: 'download'.tr(),
): (_) => handleDownload(),
},
if (isInAlbum)
{
BottomNavigationBarItem(
icon: const Icon(Icons.remove_circle_outline),
label: 'album_viewer_appbar_share_remove'.tr(),
tooltip: 'album_viewer_appbar_share_remove'.tr(),
): (_) => handleRemoveFromAlbum(),
},
];
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
@@ -407,11 +410,10 @@ class BottomGalleryBar extends ConsumerWidget {
unselectedItemColor: Colors.white,
showSelectedLabels: true,
showUnselectedLabels: true,
items: itemsList,
items:
albumActions.map((e) => e.keys.first).toList(growable: false),
onTap: (index) {
if (index < actionslist.length) {
actionslist[index].call(index);
}
albumActions[index].values.first.call(index);
},
),
],

View File

@@ -6,6 +6,7 @@ import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset_description.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -29,17 +30,16 @@ class DescriptionInput extends HookConsumerWidget {
final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty);
final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
final owner = ref.watch(currentUserProvider);
final hasError = useState(false);
final assetWithExif = ref.watch(assetDetailProvider(asset));
useEffect(
() {
controller.text = exifInfo?.description ?? '';
isTextEmpty.value = exifInfo?.description?.isEmpty ?? true;
controller.text = descriptionProvider.getAssetDescription(asset);
return null;
},
[exifInfo?.description],
[assetWithExif.value],
);
submitDescription(String description) async {
@@ -49,6 +49,7 @@ class DescriptionInput extends HookConsumerWidget {
asset,
description,
);
controller.text = description;
} catch (error, stack) {
hasError.value = true;
_log.severe("Error updating description", error, stack);
@@ -101,6 +102,11 @@ class DescriptionInput extends HookConsumerWidget {
hintText: 'description_input_hint_text'.tr(),
border: InputBorder.none,
suffixIcon: suffixIcon,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
),
);
}

View File

@@ -0,0 +1,54 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
class AssetDateTime extends ConsumerWidget {
final Asset asset;
const AssetDateTime({super.key, required this.asset});
String getDateTimeString(Asset a) {
final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset();
final date = DateFormat.yMMMEd().format(deltaTime);
final time = DateFormat.jm().format(deltaTime);
return '$date$time GMT${timeZone.formatAsOffset()}';
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final watchedAsset = ref.watch(assetDetailProvider(asset));
String formattedDateTime = getDateTimeString(asset);
void editDateTime() async {
await handleEditDateTime(ref, context, [asset]);
if (watchedAsset.value != null) {
formattedDateTime = getDateTimeString(watchedAsset.value!);
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
formattedDateTime,
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (asset.isRemote)
IconButton(
onPressed: editDateTime,
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart';
class AssetDetails extends ConsumerWidget {
final Asset asset;
final ExifInfo? exifInfo;
const AssetDetails({
super.key,
required this.asset,
this.exifInfo,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo;
return Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"exif_bottom_sheet_details",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
FileInfo(asset: asset),
if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!),
],
),
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
class AssetLocation extends HookConsumerWidget {
final Asset asset;
const AssetLocation({
super.key,
required this.asset,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo;
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
void editLocation() {
handleEditLocation(ref, context, [assetWithExif.value ?? asset]);
}
// Guard no lat/lng
if (!hasCoordinates) {
return asset.isRemote
? ListTile(
minLeadingWidth: 0,
contentPadding: const EdgeInsets.all(0),
leading: const Icon(Icons.location_on),
title: Text(
"exif_bottom_sheet_location_add",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
onTap: editLocation,
)
: const SizedBox.shrink();
}
Widget getLocationName() {
if (exifInfo == null) {
return const SizedBox.shrink();
}
final cityName = exifInfo.city;
final stateName = exifInfo.state;
bool hasLocationName = (cityName != null && stateName != null);
return hasLocationName
? Text(
"$cityName, $stateName",
style: context.textTheme.labelLarge,
)
: const SizedBox.shrink();
}
return Padding(
padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"exif_bottom_sheet_location",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
if (asset.isRemote)
IconButton(
onPressed: editLocation,
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
),
asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16),
ExifMap(
exifInfo: exifInfo!,
markerId: asset.remoteId,
),
const SizedBox(height: 16),
getLocationName(),
Text(
"${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class CameraInfo extends StatelessWidget {
final ExifInfo exifInfo;
const CameraInfo({
super.key,
required this.exifInfo,
});
@override
Widget build(BuildContext context) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
return ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.camera,
color: textColor.withAlpha(200),
),
title: Text(
"${exifInfo.make} ${exifInfo.model}",
style: context.textTheme.labelLarge,
),
subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
exifInfo.mm != null ||
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
style: context.textTheme.bodySmall,
)
: null,
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/widgets/asset_viewer/description_input.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
class DetailPanel extends HookConsumerWidget {
final Asset asset;
const DetailPanel({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
AssetDateTime(asset: asset),
asset.isRemote
? DescriptionInput(asset: asset)
: const SizedBox.shrink(),
PeopleInfo(asset: asset),
AssetLocation(asset: asset),
AssetDetails(asset: asset),
],
),
),
],
);
}
}

View File

@@ -8,13 +8,11 @@ import 'package:url_launcher/url_launcher.dart';
class ExifMap extends StatelessWidget {
final ExifInfo exifInfo;
final String formattedDateTime;
final String? markerId;
const ExifMap({
super.key,
required this.exifInfo,
required this.formattedDateTime,
this.markerId = 'marker',
});
@@ -37,7 +35,7 @@ class ExifMap extends StatelessWidget {
host: '$latitude,$longitude',
queryParameters: {
'z': '$zoomLevel',
'q': '$latitude,$longitude($formattedDateTime)',
'q': '$latitude,$longitude',
},
);
if (await canLaunchUrl(uri)) {
@@ -46,7 +44,7 @@ class ExifMap extends StatelessWidget {
} else if (Platform.isIOS) {
var params = {
'll': '$latitude,$longitude',
'q': formattedDateTime,
'q': '$latitude,$longitude',
'z': '$zoomLevel',
};
Uri uri = Uri.https('maps.apple.com', '/', params);
@@ -63,32 +61,29 @@ class ExifMap extends StatelessWidget {
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: LayoutBuilder(
builder: (context, constraints) {
return MapThumbnail(
centre: LatLng(
exifInfo.latitude ?? 0,
exifInfo.longitude ?? 0,
),
height: 150,
width: constraints.maxWidth,
zoom: 12.0,
assetMarkerRemoteId: markerId,
onTap: (tapPosition, latLong) async {
Uri? uri = await createCoordinatesUri();
return LayoutBuilder(
builder: (context, constraints) {
return MapThumbnail(
centre: LatLng(
exifInfo.latitude ?? 0,
exifInfo.longitude ?? 0,
),
height: 150,
width: constraints.maxWidth,
zoom: 12.0,
assetMarkerRemoteId: markerId,
onTap: (tapPosition, latLong) async {
Uri? uri = await createCoordinatesUri();
if (uri == null) {
return;
}
if (uri == null) {
return;
}
debugPrint('Opening Map Uri: $uri');
launchUrl(uri);
},
);
},
),
debugPrint('Opening Map Uri: $uri');
launchUrl(uri);
},
);
},
);
}
}

View File

@@ -3,10 +3,10 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
class ExifImageProperties extends StatelessWidget {
class FileInfo extends StatelessWidget {
final Asset asset;
const ExifImageProperties({
const FileInfo({
super.key,
required this.asset,
});

View File

@@ -0,0 +1,102 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
class PeopleInfo extends ConsumerWidget {
final Asset asset;
final EdgeInsets? padding;
const PeopleInfo({super.key, required this.asset, this.padding});
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleProvider =
ref.watch(assetPeopleNotifierProvider(asset).notifier);
final people = ref
.watch(assetPeopleNotifierProvider(asset))
.value
?.where((p) => !p.isHidden);
final double imageSize = math.min(context.width / 3, 150);
showPersonNameEditModel(
String personId,
String personName,
) {
return showDialog(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(personId: personId, personName: personName);
},
).then((_) {
// ensure the people list is up-to-date.
peopleProvider.refresh();
});
}
final curatedPeople = people
?.map((p) => SearchCuratedContent(id: p.id, label: p.name))
.toList() ??
[];
return AnimatedCrossFade(
crossFadeState: (people?.isEmpty ?? true)
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 200),
firstChild: Container(),
secondChild: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
children: [
Padding(
padding: padding ?? EdgeInsets.zero,
child: Align(
alignment: Alignment.topLeft,
child: Text(
"exif_bottom_sheet_people",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
),
),
SizedBox(
height: imageSize,
child: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: CuratedPeopleRow(
padding: padding,
content: curatedPeople,
onTap: (content, index) {
context
.pushRoute(
PersonResultRoute(
personId: content.id,
personName: content.label,
),
)
.then((_) => peopleProvider.refresh());
},
onNameTap: (person, index) => {
showPersonNameEditModel(person.id, person.label),
},
),
),
),
],
),
),
);
}
}

View File

@@ -1,212 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/widgets/asset_viewer/description_input.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_detail.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_location.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_people.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
class ExifBottomSheet extends HookConsumerWidget {
final Asset asset;
const ExifBottomSheet({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
var textColor = context.colorScheme.onSurface;
final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo;
// Format the date time with the timezone
final (dt, timeZone) =
(assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset();
final date = DateFormat.yMMMEd().format(dt);
final time = DateFormat.jm().format(dt);
String formattedDateTime = '$date$time GMT${timeZone.formatAsOffset()}';
final dateWidget = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
formattedDateTime,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (asset.isRemote)
IconButton(
onPressed: () => handleEditDateTime(
ref,
context,
[assetWithExif.value ?? asset],
),
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
);
return SingleChildScrollView(
padding: const EdgeInsets.only(
bottom: 50,
),
child: LayoutBuilder(
builder: (context, constraints) {
final horizontalPadding = constraints.maxWidth > 600 ? 24.0 : 16.0;
if (constraints.maxWidth > 600) {
// Two column
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Column(
children: [
dateWidget,
if (asset.isRemote)
DescriptionInput(asset: asset, exifInfo: exifInfo),
],
),
),
ExifPeople(
asset: asset,
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ExifLocation(
asset: asset,
exifInfo: exifInfo,
editLocation: () => handleEditLocation(
ref,
context,
[assetWithExif.value ?? asset],
),
formattedDateTime: formattedDateTime,
),
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ExifDetail(asset: asset, exifInfo: exifInfo),
),
),
],
),
),
],
);
}
// One column
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
),
child: Column(
children: [
dateWidget,
if (asset.isRemote)
DescriptionInput(asset: asset, exifInfo: exifInfo),
Padding(
padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0),
child: ExifLocation(
asset: asset,
exifInfo: exifInfo,
editLocation: () => handleEditLocation(
ref,
context,
[assetWithExif.value ?? asset],
),
formattedDateTime: formattedDateTime,
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ExifPeople(
asset: asset,
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color
?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
),
ExifImageProperties(asset: asset),
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.camera,
color: textColor.withAlpha(200),
),
title: Text(
"${exifInfo!.make} ${exifInfo.model}",
style: context.textTheme.labelLarge,
),
subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
exifInfo.mm != null ||
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
style: context.textTheme.bodySmall,
)
: null,
),
],
),
),
const SizedBox(height: 50),
],
);
},
),
);
}
}

View File

@@ -1,60 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
class ExifDetail extends StatelessWidget {
final Asset asset;
final ExifInfo? exifInfo;
const ExifDetail({
super.key,
required this.asset,
this.exifInfo,
});
@override
Widget build(BuildContext context) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
),
ExifImageProperties(asset: asset),
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.camera,
color: textColor.withAlpha(200),
),
title: Text(
"${exifInfo?.make} ${exifInfo?.model}",
style: context.textTheme.labelLarge,
),
subtitle: exifInfo?.f != null ||
exifInfo?.exposureSeconds != null ||
exifInfo?.mm != null ||
exifInfo?.iso != null
? Text(
"ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ",
style: context.textTheme.bodySmall,
)
: null,
),
],
);
}
}

View File

@@ -1,105 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_map.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
class ExifLocation extends StatelessWidget {
final Asset asset;
final ExifInfo? exifInfo;
final void Function() editLocation;
final String formattedDateTime;
const ExifLocation({
super.key,
required this.asset,
required this.exifInfo,
required this.editLocation,
required this.formattedDateTime,
});
@override
Widget build(BuildContext context) {
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
// Guard no lat/lng
if (!hasCoordinates) {
return asset.isRemote
? ListTile(
minLeadingWidth: 0,
contentPadding: const EdgeInsets.all(0),
leading: const Icon(Icons.location_on),
title: Text(
"exif_bottom_sheet_location_add",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
onTap: editLocation,
)
: const SizedBox.shrink();
}
return Column(
children: [
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"exif_bottom_sheet_location",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
if (asset.isRemote)
IconButton(
onPressed: editLocation,
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
],
),
ExifMap(
exifInfo: exifInfo!,
formattedDateTime: formattedDateTime,
markerId: asset.remoteId,
),
RichText(
text: TextSpan(
style: context.textTheme.labelLarge,
children: [
if (exifInfo != null && exifInfo?.city != null)
TextSpan(
text: exifInfo!.city,
),
if (exifInfo != null &&
exifInfo?.city != null &&
exifInfo?.state != null)
const TextSpan(
text: ", ",
),
if (exifInfo != null && exifInfo?.state != null)
TextSpan(
text: exifInfo!.state,
),
],
),
),
Text(
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
),
],
),
],
);
}
}

View File

@@ -1,97 +0,0 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
class ExifPeople extends ConsumerWidget {
final Asset asset;
final EdgeInsets? padding;
const ExifPeople({super.key, required this.asset, this.padding});
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleProvider =
ref.watch(assetPeopleNotifierProvider(asset).notifier);
final people = ref
.watch(assetPeopleNotifierProvider(asset))
.value
?.where((p) => !p.isHidden);
final double imageSize = math.min(context.width / 3, 150);
showPersonNameEditModel(
String personId,
String personName,
) {
return showDialog(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(personId: personId, personName: personName);
},
).then((_) {
// ensure the people list is up-to-date.
peopleProvider.refresh();
});
}
if (people?.isEmpty ?? true) {
// Empty list or loading
return Container();
}
final curatedPeople = people
?.map((p) => SearchCuratedContent(id: p.id, label: p.name))
.toList() ??
[];
return Column(
children: [
Padding(
padding: padding ?? EdgeInsets.zero,
child: Align(
alignment: Alignment.topLeft,
child: Text(
"exif_bottom_sheet_people",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
),
),
SizedBox(
height: imageSize,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: CuratedPeopleRow(
padding: padding,
content: curatedPeople,
onTap: (content, index) {
context
.pushRoute(
PersonResultRoute(
personId: content.id,
personName: content.label,
),
)
.then((_) => peopleProvider.refresh());
},
onNameTap: (person, index) => {
showPersonNameEditModel(person.id, person.label),
},
),
),
),
],
);
}
}

View File

@@ -84,6 +84,19 @@ class _DateTimePicker extends HookWidget {
final date = useState<DateTime>(initialDateTime ?? DateTime.now());
final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation());
final timeZones = useMemoized(() => getAllTimeZones(), const []);
final menuEntries = timeZones
.map(
(timezone) => DropdownMenuEntry<_TimeZoneOffset>(
value: timezone,
label: timezone.display,
style: ButtonStyle(
textStyle: WidgetStatePropertyAll(
context.textTheme.bodyMedium,
),
),
),
)
.toList();
void pickDate() async {
final now = DateTime.now();
@@ -120,93 +133,84 @@ class _DateTimePicker extends HookWidget {
context.pop(dtWithOffset);
}
return AlertDialog(
contentPadding: const EdgeInsets.all(30),
alignment: Alignment.center,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_date_time_dialog_date_time",
textAlign: TextAlign.center,
).tr(),
TextButton.icon(
onPressed: pickDate,
icon: Text(
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
style: context.textTheme.bodyLarge
?.copyWith(color: context.primaryColor),
),
label: const Icon(
Icons.edit_outlined,
size: 18,
),
return LayoutBuilder(
builder: (context, constraint) => AlertDialog(
contentPadding:
const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(
"action_common_cancel",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colorScheme.error,
),
).tr(),
),
const Text(
"edit_date_time_dialog_timezone",
textAlign: TextAlign.center,
).tr(),
DropdownMenu(
menuHeight: 300,
width: 280,
inputDecorationTheme: const InputDecorationTheme(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
TextButton(
onPressed: popWithDateTime,
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
),
],
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"edit_date_time_dialog_date_time",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
).tr(),
const SizedBox(height: 32),
ListTile(
tileColor: context.colorScheme.surfaceContainerHighest,
shape: ShapeBorder.lerp(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
1,
),
trailing: Icon(
Icons.edit_outlined,
size: 18,
color: context.primaryColor,
),
title: Text(
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
style: context.textTheme.bodyMedium,
).tr(),
onTap: pickDate,
),
trailingIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: Icon(
const SizedBox(height: 24),
DropdownMenu(
width: 275,
menuHeight: 300,
trailingIcon: Icon(
Icons.arrow_drop_down,
color: context.primaryColor,
),
hintText: "edit_date_time_dialog_timezone".tr(),
label: const Text('edit_date_time_dialog_timezone').tr(),
textStyle: context.textTheme.bodyMedium,
onSelected: (value) => tzOffset.value = value!,
initialSelection: tzOffset.value,
dropdownMenuEntries: menuEntries,
),
textStyle: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
menuStyle: const MenuStyle(
fixedSize: WidgetStatePropertyAll(Size.fromWidth(350)),
alignment: Alignment(-1.25, 0.5),
),
onSelected: (value) => tzOffset.value = value!,
initialSelection: tzOffset.value,
dropdownMenuEntries: timeZones
.map(
(t) => DropdownMenuEntry<_TimeZoneOffset>(
value: t,
label: t.display,
style: ButtonStyle(
textStyle: WidgetStatePropertyAll(
context.textTheme.bodyMedium,
),
),
),
)
.toList(),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(
"action_common_cancel",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colorScheme.error,
),
).tr(),
),
TextButton(
onPressed: popWithDateTime,
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
),
],
);
}
}

View File

@@ -65,8 +65,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
size: widgetSize,
)
: UserCircleAvatar(
radius: 15,
size: 27,
radius: 17,
size: 31,
user: user,
),
),

View File

@@ -16,7 +16,6 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -27,8 +26,8 @@ import 'package:immich_mobile/widgets/forms/login/login_button.dart';
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
import 'package:immich_mobile/widgets/forms/login/version_compatibility_warning.dart';
import 'package:openapi/api.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget {
@@ -54,31 +53,10 @@ class LoginForm extends HookConsumerWidget {
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
final serverInfo = ref.watch(serverInfoProvider);
final warningMessage = useState<String?>(null);
final loginFormKey = GlobalKey<FormState>();
final ValueNotifier<String?> serverEndpoint = useState<String?>(null);
checkVersionMismatch() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
final appMajorVersion = int.parse(appVersion.split('.')[0]);
final appMinorVersion = int.parse(appVersion.split('.')[1]);
final serverMajorVersion = serverInfo.serverVersion.major;
final serverMinorVersion = serverInfo.serverVersion.minor;
warningMessage.value = getVersionCompatibilityMessage(
appMajorVersion,
appMinorVersion,
serverMajorVersion,
serverMinorVersion,
);
} catch (error) {
warningMessage.value = 'Error checking version compatibility';
}
}
/// Fetch the server login credential and enables oAuth login if necessary
/// Returns true if successful, false otherwise
Future<bool> getServerLoginCredential() async {
@@ -340,40 +318,12 @@ class LoginForm extends HookConsumerWidget {
);
}
buildVersionCompatWarning() {
checkVersionMismatch();
if (warningMessage.value == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!,
),
),
child: Text(
warningMessage.value!,
textAlign: TextAlign.center,
),
),
);
}
buildLogin() {
return AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildVersionCompatWarning(),
const VersionCompatibilityWarning(),
Text(
sanitizeUrl(serverEndpointController.text),
style: context.textTheme.displaySmall,

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.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/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:package_info_plus/package_info_plus.dart';
class VersionCompatibilityWarning extends HookConsumerWidget {
const VersionCompatibilityWarning({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final warningMessage = useState<String?>(null);
final serverInfo = ref.watch(serverInfoProvider);
checkVersionMismatch() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
final appMajorVersion = int.parse(appVersion.split('.')[0]);
final appMinorVersion = int.parse(appVersion.split('.')[1]);
final serverMajorVersion = serverInfo.serverVersion.major;
final serverMinorVersion = serverInfo.serverVersion.minor;
warningMessage.value = getVersionCompatibilityMessage(
appMajorVersion,
appMinorVersion,
serverMajorVersion,
serverMinorVersion,
);
} catch (error) {
warningMessage.value = 'Error checking version compatibility';
}
}
useEffect(
() {
checkVersionMismatch();
return () {};
},
[],
);
return warningMessage.value == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: context.isDarkTheme
? Colors.red.shade700
: Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDarkTheme
? Colors.red.shade900
: Colors.red[200]!,
),
),
child: Text(
warningMessage.value!,
textAlign: TextAlign.center,
),
),
);
}
}

View File

@@ -18,13 +18,6 @@ class SearchDropdown<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final inputDecorationTheme = InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
);
final menuStyle = MenuStyle(
shape: WidgetStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
@@ -40,7 +33,6 @@ class SearchDropdown<T> extends StatelessWidget {
width: constraints.maxWidth,
dropdownMenuEntries: dropdownMenuEntries,
label: label,
inputDecorationTheme: inputDecorationTheme,
menuStyle: menuStyle,
trailingIcon: const Icon(Icons.arrow_drop_down_rounded),
selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),

View File

@@ -372,6 +372,8 @@ Class | Method | HTTP request | Description
- [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [RatingResponse](doc//RatingResponse.md)
- [RatingUpdate](doc//RatingUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)

View File

@@ -16,6 +16,7 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/openapi_patching.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';
@@ -183,6 +184,8 @@ part 'model/places_response_dto.dart';
part 'model/purchase_response.dart';
part 'model/purchase_update.dart';
part 'model/queue_status_dto.dart';
part 'model/rating_response.dart';
part 'model/rating_update.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
part 'model/reverse_geocoding_state_response_dto.dart';

View File

@@ -166,6 +166,7 @@ class ApiClient {
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
upgradeDto(value, targetType);
try {
switch (targetType) {
case 'String':
@@ -424,6 +425,10 @@ class ApiClient {
return PurchaseUpdate.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'RatingResponse':
return RatingResponse.fromJson(value);
case 'RatingUpdate':
return RatingUpdate.fromJson(value);
case 'ReactionLevel':
return ReactionLevelTypeTransformer().decode(value);
case 'ReactionType':

View File

@@ -20,6 +20,7 @@ class AssetBulkUpdateDto {
this.isFavorite,
this.latitude,
this.longitude,
this.rating,
this.removeParent,
this.stackParentId,
});
@@ -68,6 +69,16 @@ class AssetBulkUpdateDto {
///
num? longitude;
/// Minimum value: 0
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -93,6 +104,7 @@ class AssetBulkUpdateDto {
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
other.rating == rating &&
other.removeParent == removeParent &&
other.stackParentId == stackParentId;
@@ -106,11 +118,12 @@ class AssetBulkUpdateDto {
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode) +
(removeParent == null ? 0 : removeParent!.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode);
@override
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, removeParent=$removeParent, stackParentId=$stackParentId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -145,6 +158,11 @@ class AssetBulkUpdateDto {
} else {
// json[r'longitude'] = null;
}
if (this.rating != null) {
json[r'rating'] = this.rating;
} else {
// json[r'rating'] = null;
}
if (this.removeParent != null) {
json[r'removeParent'] = this.removeParent;
} else {
@@ -175,6 +193,7 @@ class AssetBulkUpdateDto {
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
removeParent: mapValueOfType<bool>(json, r'removeParent'),
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
);

View File

@@ -32,6 +32,7 @@ class ExifResponseDto {
this.modifyDate,
this.orientation,
this.projectionType,
this.rating,
this.state,
this.timeZone,
});
@@ -74,6 +75,8 @@ class ExifResponseDto {
String? projectionType;
num? rating;
String? state;
String? timeZone;
@@ -99,6 +102,7 @@ class ExifResponseDto {
other.modifyDate == modifyDate &&
other.orientation == orientation &&
other.projectionType == projectionType &&
other.rating == rating &&
other.state == state &&
other.timeZone == timeZone;
@@ -124,11 +128,12 @@ class ExifResponseDto {
(modifyDate == null ? 0 : modifyDate!.hashCode) +
(orientation == null ? 0 : orientation!.hashCode) +
(projectionType == null ? 0 : projectionType!.hashCode) +
(rating == null ? 0 : rating!.hashCode) +
(state == null ? 0 : state!.hashCode) +
(timeZone == null ? 0 : timeZone!.hashCode);
@override
String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, state=$state, timeZone=$timeZone]';
String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, rating=$rating, state=$state, timeZone=$timeZone]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -227,6 +232,11 @@ class ExifResponseDto {
} else {
// json[r'projectionType'] = null;
}
if (this.rating != null) {
json[r'rating'] = this.rating;
} else {
// json[r'rating'] = null;
}
if (this.state != null) {
json[r'state'] = this.state;
} else {
@@ -281,6 +291,9 @@ class ExifResponseDto {
modifyDate: mapDateTime(json, r'modifyDate', r''),
orientation: mapValueOfType<String>(json, r'orientation'),
projectionType: mapValueOfType<String>(json, r'projectionType'),
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'),
timeZone: mapValueOfType<String>(json, r'timeZone'),
);

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ class UpdateAssetDto {
this.isFavorite,
this.latitude,
this.longitude,
this.rating,
});
///
@@ -69,6 +70,16 @@ class UpdateAssetDto {
///
num? longitude;
/// Minimum value: 0
/// Maximum value: 5
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.dateTimeOriginal == dateTimeOriginal &&
@@ -76,7 +87,8 @@ class UpdateAssetDto {
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude;
other.longitude == longitude &&
other.rating == rating;
@override
int get hashCode =>
@@ -86,10 +98,11 @@ class UpdateAssetDto {
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode);
(longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode);
@override
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -123,6 +136,11 @@ class UpdateAssetDto {
} else {
// json[r'longitude'] = null;
}
if (this.rating != null) {
json[r'rating'] = this.rating;
} else {
// json[r'rating'] = null;
}
return json;
}
@@ -140,6 +158,7 @@ class UpdateAssetDto {
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
);
}
return null;

View File

@@ -18,6 +18,7 @@ class UserPreferencesResponseDto {
required this.emailNotifications,
required this.memories,
required this.purchase,
required this.rating,
});
AvatarResponse avatar;
@@ -30,13 +31,16 @@ class UserPreferencesResponseDto {
PurchaseResponse purchase;
RatingResponse rating;
@override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
other.avatar == avatar &&
other.download == download &&
other.emailNotifications == emailNotifications &&
other.memories == memories &&
other.purchase == purchase;
other.purchase == purchase &&
other.rating == rating;
@override
int get hashCode =>
@@ -45,10 +49,11 @@ class UserPreferencesResponseDto {
(download.hashCode) +
(emailNotifications.hashCode) +
(memories.hashCode) +
(purchase.hashCode);
(purchase.hashCode) +
(rating.hashCode);
@override
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]';
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -57,6 +62,7 @@ class UserPreferencesResponseDto {
json[r'emailNotifications'] = this.emailNotifications;
json[r'memories'] = this.memories;
json[r'purchase'] = this.purchase;
json[r'rating'] = this.rating;
return json;
}
@@ -73,6 +79,7 @@ class UserPreferencesResponseDto {
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
memories: MemoryResponse.fromJson(json[r'memories'])!,
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
rating: RatingResponse.fromJson(json[r'rating'])!,
);
}
return null;
@@ -125,6 +132,7 @@ class UserPreferencesResponseDto {
'emailNotifications',
'memories',
'purchase',
'rating',
};
}

View File

@@ -18,6 +18,7 @@ class UserPreferencesUpdateDto {
this.emailNotifications,
this.memories,
this.purchase,
this.rating,
});
///
@@ -60,13 +61,22 @@ class UserPreferencesUpdateDto {
///
PurchaseUpdate? purchase;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
RatingUpdate? rating;
@override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto &&
other.avatar == avatar &&
other.download == download &&
other.emailNotifications == emailNotifications &&
other.memories == memories &&
other.purchase == purchase;
other.purchase == purchase &&
other.rating == rating;
@override
int get hashCode =>
@@ -75,10 +85,11 @@ class UserPreferencesUpdateDto {
(download == null ? 0 : download!.hashCode) +
(emailNotifications == null ? 0 : emailNotifications!.hashCode) +
(memories == null ? 0 : memories!.hashCode) +
(purchase == null ? 0 : purchase!.hashCode);
(purchase == null ? 0 : purchase!.hashCode) +
(rating == null ? 0 : rating!.hashCode);
@override
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]';
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -107,6 +118,11 @@ class UserPreferencesUpdateDto {
} else {
// json[r'purchase'] = null;
}
if (this.rating != null) {
json[r'rating'] = this.rating;
} else {
// json[r'rating'] = null;
}
return json;
}
@@ -123,6 +139,7 @@ class UserPreferencesUpdateDto {
emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']),
memories: MemoryUpdate.fromJson(json[r'memories']),
purchase: PurchaseUpdate.fromJson(json[r'purchase']),
rating: RatingUpdate.fromJson(json[r'rating']),
);
}
return null;

View File

@@ -13,5 +13,5 @@ dependencies:
http: '>=0.13.0 <0.14.0'
intl: any
meta: '^1.1.8'
dev_dependencies:
test: '>=1.21.6 <1.22.0'
immich_mobile:
path: ../

View File

@@ -8,12 +8,18 @@ function dart {
cd ./templates/mobile/serialization/native
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
cd ../../../..
cd ../../
wget -O api_client.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api_client.mustache
patch --no-backup-if-mismatch -u api_client.mustache <api_client.mustache.patch
cd ../../
npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
# Post generate patches
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
# Don't include analysis_options.yaml for the generated openapi files
# so that language servers can properly exclude the mobile/openapi directory
rm ../mobile/openapi/analysis_options.yaml
@@ -34,4 +40,4 @@ elif [[ $1 == 'typescript' ]]; then
else
dart
typescript
fi
fi

View File

@@ -7550,6 +7550,11 @@
"longitude": {
"type": "number"
},
"rating": {
"maximum": 5,
"minimum": 0,
"type": "number"
},
"removeParent": {
"type": "boolean"
},
@@ -8702,6 +8707,11 @@
"nullable": true,
"type": "string"
},
"rating": {
"default": null,
"nullable": true,
"type": "number"
},
"state": {
"default": null,
"nullable": true,
@@ -9905,6 +9915,26 @@
],
"type": "object"
},
"RatingResponse": {
"properties": {
"enabled": {
"default": false,
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"RatingUpdate": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"ReactionLevel": {
"enum": [
"album",
@@ -11565,6 +11595,11 @@
},
"longitude": {
"type": "number"
},
"rating": {
"maximum": 5,
"minimum": 0,
"type": "number"
}
},
"type": "object"
@@ -11865,6 +11900,9 @@
},
"purchase": {
"$ref": "#/components/schemas/PurchaseResponse"
},
"rating": {
"$ref": "#/components/schemas/RatingResponse"
}
},
"required": [
@@ -11872,7 +11910,8 @@
"download",
"emailNotifications",
"memories",
"purchase"
"purchase",
"rating"
],
"type": "object"
},
@@ -11892,6 +11931,9 @@
},
"purchase": {
"$ref": "#/components/schemas/PurchaseUpdate"
},
"rating": {
"$ref": "#/components/schemas/RatingUpdate"
}
},
"type": "object"

View File

@@ -1,8 +1,9 @@
@@ -15,6 +15,7 @@
@@ -15,6 +15,8 @@
import 'dart:io';
import 'package:collection/collection.dart';
+import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/utils/openapi_patching.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';

View File

@@ -0,0 +1,9 @@
# Include code from immich_mobile
@@ -13,5 +13,5 @@
http: '>=0.13.0 <0.14.0'
intl: any
meta: '^1.1.8'
-dev_dependencies:
- test: '>=1.21.6 <1.22.0'
+ immich_mobile:
+ path: ../

View File

@@ -0,0 +1,264 @@
{{>header}}
{{>part_of}}
class ApiClient {
ApiClient({this.basePath = '{{{basePath}}}', this.authentication,});
final String basePath;
final Authentication? authentication;
var _client = Client();
final _defaultHeaderMap = <String, String>{};
/// Returns the current HTTP [Client] instance to use in this class.
///
/// The return value is guaranteed to never be null.
Client get client => _client;
/// Requests to use a new HTTP [Client] in this class.
set client(Client newClient) {
_client = newClient;
}
Map<String, String> get defaultHeaderMap => _defaultHeaderMap;
void addDefaultHeader(String key, String value) {
_defaultHeaderMap[key] = value;
}
// We don't use a Map<String, String> for queryParams.
// If collectionFormat is 'multi', a key might appear multiple times.
Future<Response> invokeAPI(
String path,
String method,
List<QueryParam> queryParams,
Object? body,
Map<String, String> headerParams,
Map<String, String> formParams,
String? contentType,
) async {
await authentication?.applyToParams(queryParams, headerParams);
headerParams.addAll(_defaultHeaderMap);
if (contentType != null) {
headerParams['Content-Type'] = contentType;
}
final urlEncodedQueryParams = queryParams.map((param) => '$param');
final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : '';
final uri = Uri.parse('$basePath$path$queryString');
try {
// Special case for uploading a single file which isn't a 'multipart/form-data'.
if (
body is MultipartFile && (contentType == null ||
!contentType.toLowerCase().startsWith('multipart/form-data'))
) {
final request = StreamedRequest(method, uri);
request.headers.addAll(headerParams);
request.contentLength = body.length;
body.finalize().listen(
request.sink.add,
onDone: request.sink.close,
// ignore: avoid_types_on_closure_parameters
onError: (Object error, StackTrace trace) => request.sink.close(),
cancelOnError: true,
);
final response = await _client.send(request);
return Response.fromStream(response);
}
if (body is MultipartRequest) {
final request = MultipartRequest(method, uri);
request.fields.addAll(body.fields);
request.files.addAll(body.files);
request.headers.addAll(body.headers);
request.headers.addAll(headerParams);
final response = await _client.send(request);
return Response.fromStream(response);
}
final msgBody = contentType == 'application/x-www-form-urlencoded'
? formParams
: await serializeAsync(body);
final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
switch(method) {
case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
}
} on SocketException catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
'Socket operation failed: $method $path',
error,
trace,
);
} on TlsException catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
'TLS/SSL communication failed: $method $path',
error,
trace,
);
} on IOException catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
'I/O operation failed: $method $path',
error,
trace,
);
} on ClientException catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
'HTTP connection failed: $method $path',
error,
trace,
);
} on Exception catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
'Exception occurred: $method $path',
error,
trace,
);
}
throw ApiException(
HttpStatus.badRequest,
'Invalid HTTP operation: $method $path',
);
}
{{#native_serialization}}
Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
// ignore: deprecated_member_use_from_same_package
deserialize(value, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
dynamic deserialize(String value, String targetType, {bool growable = false,}) {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? value
: fromJson(json.decode(value), targetType, growable: growable);
}
{{/native_serialization}}
// ignore: deprecated_member_use_from_same_package
Future<String> serializeAsync(Object? value) async => serialize(value);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.')
String serialize(Object? value) => value == null ? '' : json.encode(value);
{{#native_serialization}}
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
upgradeDto(value, targetType);
try {
switch (targetType) {
case 'String':
return value is String ? value : value.toString();
case 'int':
return value is int ? value : int.parse('$value');
case 'double':
return value is double ? value : double.parse('$value');
case 'bool':
if (value is bool) {
return value;
}
final valueString = '$value'.toLowerCase();
return valueString == 'true' || valueString == '1';
case 'DateTime':
return value is DateTime ? value : DateTime.tryParse(value);
{{#models}}
{{#model}}
case '{{{classname}}}':
{{#isEnum}}
{{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}}
{{/isEnum}}
{{^isEnum}}
return {{{classname}}}.fromJson(value);
{{/isEnum}}
{{/model}}
{{/models}}
default:
dynamic match;
if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) {
return value
.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
.toList(growable: growable);
}
if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) {
return value
.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
.toSet();
}
if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) {
return Map<String, dynamic>.fromIterables(
value.keys.cast<String>(),
value.values.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,)),
);
}
}
} on Exception catch (error, trace) {
throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,);
}
throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',);
}
{{/native_serialization}}
}
{{#native_serialization}}
/// Primarily intended for use in an isolate.
class DeserializationMessage {
const DeserializationMessage({
required this.json,
required this.targetType,
this.growable = false,
});
/// The JSON value to deserialize.
final String json;
/// Target type to deserialize to.
final String targetType;
/// Whether to make deserialized lists or maps growable.
final bool growable;
}
/// Primarily intended for use in an isolate.
Future<dynamic> decodeAsync(DeserializationMessage message) async {
// Remove all spaces. Necessary for regular expressions as well.
final targetType = message.targetType.replaceAll(' ', '');
// If the expected target type is String, nothing to do...
return targetType == 'String'
? message.json
: json.decode(message.json);
}
/// Primarily intended for use in an isolate.
Future<dynamic> deserializeAsync(DeserializationMessage message) async {
// Remove all spaces. Necessary for regular expressions as well.
final targetType = message.targetType.replaceAll(' ', '');
// If the expected target type is String, nothing to do...
return targetType == 'String'
? message.json
: ApiClient.fromJson(
json.decode(message.json),
targetType,
growable: message.growable,
);
}
{{/native_serialization}}
/// Primarily intended for use in an isolate.
Future<String> serializeAsync(Object? value) async => value == null ? '' : json.encode(value);

View File

@@ -0,0 +1,10 @@
--- api_client.mustache 2024-08-13 14:29:04.056364916 -0500
+++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500
@@ -159,6 +159,7 @@
{{#native_serialization}}
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
+ upgradeDto(value, targetType);
try {
switch (targetType) {
case 'String':

View File

@@ -12,7 +12,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"typescript": "^5.3.3"
}
},
@@ -22,9 +22,9 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
},
"node_modules/@types/node": {
"version": "20.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
"integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"typescript": "^5.3.3"
},
"repository": {

View File

@@ -99,12 +99,16 @@ export type PurchaseResponse = {
hideBuyButtonUntil: string;
showSupportBadge: boolean;
};
export type RatingResponse = {
enabled: boolean;
};
export type UserPreferencesResponseDto = {
avatar: AvatarResponse;
download: DownloadResponse;
emailNotifications: EmailNotificationsResponse;
memories: MemoryResponse;
purchase: PurchaseResponse;
rating: RatingResponse;
};
export type AvatarUpdate = {
color?: UserAvatarColor;
@@ -124,12 +128,16 @@ export type PurchaseUpdate = {
hideBuyButtonUntil?: string;
showSupportBadge?: boolean;
};
export type RatingUpdate = {
enabled?: boolean;
};
export type UserPreferencesUpdateDto = {
avatar?: AvatarUpdate;
download?: DownloadUpdate;
emailNotifications?: EmailNotificationsUpdate;
memories?: MemoryUpdate;
purchase?: PurchaseUpdate;
rating?: RatingUpdate;
};
export type AlbumUserResponseDto = {
role: AlbumUserRole;
@@ -155,6 +163,7 @@ export type ExifResponseDto = {
modifyDate?: string | null;
orientation?: string | null;
projectionType?: string | null;
rating?: number | null;
state?: string | null;
timeZone?: string | null;
};
@@ -330,6 +339,7 @@ export type AssetBulkUpdateDto = {
isFavorite?: boolean;
latitude?: number;
longitude?: number;
rating?: number;
removeParent?: boolean;
stackParentId?: string;
};
@@ -381,6 +391,7 @@ export type UpdateAssetDto = {
isFavorite?: boolean;
latitude?: number;
longitude?: number;
rating?: number;
};
export type AssetMediaReplaceDto = {
assetData: Blob;

View File

@@ -81,5 +81,5 @@
],
"ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"],
"ignoreDeps": ["http", "intl"],
"labels": ["dependencies", "renovate"]
"labels": ["dependencies"]
}

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240806@sha256:357c0e3a6b3cece3af7e9c46f5a2d11b6f032ded6a5b1de7706acf785b85a873 AS dev
FROM ghcr.io/immich-app/base-server-dev:20240813@sha256:2e204a2256c088c9e4a0cf34cc9f70f9196c05e8744004000e7d2889466fc735 AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240806@sha256:c13555680d8b454a416fa0e8c0e9e33b348433793c29680231e83b08838f06ec
FROM ghcr.io/immich-app/base-server-prod:20240813@sha256:51537e98ac601aa8401604a6aa9421e94aa55e03c303f355cc5870142adcc471
WORKDIR /usr/src/app
ENV NODE_ENV=production \

239
server/package-lock.json generated
View File

@@ -83,7 +83,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/semver": "^7.5.8",
@@ -107,7 +107,7 @@
"typescript": "^5.3.3",
"unplugin-swc": "^1.4.5",
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5"
}
},
@@ -5483,9 +5483,9 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"node_modules/@swc/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz",
"integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz",
"integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
@@ -5500,16 +5500,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.7.5",
"@swc/core-darwin-x64": "1.7.5",
"@swc/core-linux-arm-gnueabihf": "1.7.5",
"@swc/core-linux-arm64-gnu": "1.7.5",
"@swc/core-linux-arm64-musl": "1.7.5",
"@swc/core-linux-x64-gnu": "1.7.5",
"@swc/core-linux-x64-musl": "1.7.5",
"@swc/core-win32-arm64-msvc": "1.7.5",
"@swc/core-win32-ia32-msvc": "1.7.5",
"@swc/core-win32-x64-msvc": "1.7.5"
"@swc/core-darwin-arm64": "1.7.6",
"@swc/core-darwin-x64": "1.7.6",
"@swc/core-linux-arm-gnueabihf": "1.7.6",
"@swc/core-linux-arm64-gnu": "1.7.6",
"@swc/core-linux-arm64-musl": "1.7.6",
"@swc/core-linux-x64-gnu": "1.7.6",
"@swc/core-linux-x64-musl": "1.7.6",
"@swc/core-win32-arm64-msvc": "1.7.6",
"@swc/core-win32-ia32-msvc": "1.7.6",
"@swc/core-win32-x64-msvc": "1.7.6"
},
"peerDependencies": {
"@swc/helpers": "*"
@@ -5521,9 +5521,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz",
"integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz",
"integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==",
"cpu": [
"arm64"
],
@@ -5537,9 +5537,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz",
"integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz",
"integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==",
"cpu": [
"x64"
],
@@ -5553,9 +5553,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz",
"integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz",
"integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==",
"cpu": [
"arm"
],
@@ -5569,9 +5569,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz",
"integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz",
"integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==",
"cpu": [
"arm64"
],
@@ -5585,9 +5585,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz",
"integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz",
"integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==",
"cpu": [
"arm64"
],
@@ -5601,9 +5601,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz",
"integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz",
"integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==",
"cpu": [
"x64"
],
@@ -5617,9 +5617,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz",
"integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz",
"integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==",
"cpu": [
"x64"
],
@@ -5633,9 +5633,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz",
"integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz",
"integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==",
"cpu": [
"arm64"
],
@@ -5649,9 +5649,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz",
"integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz",
"integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==",
"cpu": [
"ia32"
],
@@ -5665,9 +5665,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz",
"integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz",
"integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==",
"cpu": [
"x64"
],
@@ -6014,9 +6014,9 @@
}
},
"node_modules/@types/node": {
"version": "20.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
"integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -8246,6 +8246,14 @@
"node": ">=12.0.0"
}
},
"node_modules/cron/node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"engines": {
"node": ">=12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -10993,9 +11001,9 @@
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
"engines": {
"node": ">=12"
}
@@ -11512,9 +11520,9 @@
}
},
"node_modules/nestjs-cls": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz",
"integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz",
"integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==",
"engines": {
"node": ">=16"
},
@@ -16240,9 +16248,9 @@
}
},
"node_modules/vite-tsconfig-paths": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
"integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
@@ -19888,92 +19896,92 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"@swc/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz",
"integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz",
"integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==",
"devOptional": true,
"requires": {
"@swc/core-darwin-arm64": "1.7.5",
"@swc/core-darwin-x64": "1.7.5",
"@swc/core-linux-arm-gnueabihf": "1.7.5",
"@swc/core-linux-arm64-gnu": "1.7.5",
"@swc/core-linux-arm64-musl": "1.7.5",
"@swc/core-linux-x64-gnu": "1.7.5",
"@swc/core-linux-x64-musl": "1.7.5",
"@swc/core-win32-arm64-msvc": "1.7.5",
"@swc/core-win32-ia32-msvc": "1.7.5",
"@swc/core-win32-x64-msvc": "1.7.5",
"@swc/core-darwin-arm64": "1.7.6",
"@swc/core-darwin-x64": "1.7.6",
"@swc/core-linux-arm-gnueabihf": "1.7.6",
"@swc/core-linux-arm64-gnu": "1.7.6",
"@swc/core-linux-arm64-musl": "1.7.6",
"@swc/core-linux-x64-gnu": "1.7.6",
"@swc/core-linux-x64-musl": "1.7.6",
"@swc/core-win32-arm64-msvc": "1.7.6",
"@swc/core-win32-ia32-msvc": "1.7.6",
"@swc/core-win32-x64-msvc": "1.7.6",
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.12"
}
},
"@swc/core-darwin-arm64": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz",
"integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz",
"integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==",
"dev": true,
"optional": true
},
"@swc/core-darwin-x64": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz",
"integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz",
"integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm-gnueabihf": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz",
"integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz",
"integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm64-gnu": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz",
"integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz",
"integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm64-musl": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz",
"integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz",
"integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==",
"dev": true,
"optional": true
},
"@swc/core-linux-x64-gnu": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz",
"integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz",
"integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==",
"dev": true,
"optional": true
},
"@swc/core-linux-x64-musl": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz",
"integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz",
"integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==",
"dev": true,
"optional": true
},
"@swc/core-win32-arm64-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz",
"integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz",
"integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==",
"dev": true,
"optional": true
},
"@swc/core-win32-ia32-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz",
"integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz",
"integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==",
"dev": true,
"optional": true
},
"@swc/core-win32-x64-msvc": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz",
"integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz",
"integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==",
"dev": true,
"optional": true
},
@@ -20302,9 +20310,9 @@
}
},
"@types/node": {
"version": "20.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
"integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -21968,6 +21976,13 @@
"requires": {
"@types/luxon": "~3.4.0",
"luxon": "~3.4.0"
},
"dependencies": {
"luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA=="
}
}
},
"cron-parser": {
@@ -23994,9 +24009,9 @@
}
},
"luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA=="
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="
},
"magic-string": {
"version": "0.30.8",
@@ -24390,9 +24405,9 @@
}
},
"nestjs-cls": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz",
"integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz",
"integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==",
"requires": {}
},
"nestjs-otel": {
@@ -27486,9 +27501,9 @@
}
},
"vite-tsconfig-paths": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
"integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
"dev": true,
"requires": {
"debug": "^4.1.1",

View File

@@ -109,7 +109,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/semver": "^7.5.8",
@@ -133,7 +133,7 @@
"typescript": "^5.3.3",
"unplugin-swc": "^1.4.5",
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5"
},
"volta": {

View File

@@ -9,6 +9,8 @@ import {
IsNotEmpty,
IsPositive,
IsString,
Max,
Min,
ValidateIf,
} from 'class-validator';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
@@ -46,6 +48,12 @@ export class UpdateAssetBase {
@IsLongitude()
@IsNotEmpty()
longitude?: number;
@Optional()
@IsInt()
@Max(5)
@Min(0)
rating?: number;
}
export class AssetBulkUpdateDto extends UpdateAssetBase {

View File

@@ -25,6 +25,7 @@ export class ExifResponseDto {
country?: string | null = null;
description?: string | null = null;
projectionType?: string | null = null;
rating?: number | null = null;
}
export function mapExif(entity: ExifEntity): ExifResponseDto {
@@ -50,6 +51,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
country: entity.country,
description: entity.description,
projectionType: entity.projectionType,
rating: entity.rating,
};
}
@@ -62,5 +64,6 @@ export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
projectionType: entity.projectionType,
exifImageWidth: entity.exifImageWidth,
exifImageHeight: entity.exifImageHeight,
rating: entity.rating,
};
}

View File

@@ -16,6 +16,11 @@ class MemoryUpdate {
enabled?: boolean;
}
class RatingUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
}
class EmailNotificationsUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@@ -45,6 +50,11 @@ class PurchaseUpdate {
}
export class UserPreferencesUpdateDto {
@Optional()
@ValidateNested()
@Type(() => RatingUpdate)
rating?: RatingUpdate;
@Optional()
@ValidateNested()
@Type(() => AvatarUpdate)
@@ -76,6 +86,10 @@ class AvatarResponse {
color!: UserAvatarColor;
}
class RatingResponse {
enabled: boolean = false;
}
class MemoryResponse {
enabled!: boolean;
}
@@ -97,6 +111,7 @@ class PurchaseResponse {
}
export class UserPreferencesResponseDto implements UserPreferences {
rating!: RatingResponse;
memories!: MemoryResponse;
avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse;

View File

@@ -95,6 +95,9 @@ export class ExifEntity {
@Column({ type: 'integer', nullable: true })
bitsPerSample!: number | null;
@Column({ type: 'integer', nullable: true })
rating!: number | null;
/* Video info */
@Column({ type: 'float8', nullable: true })
fps?: number | null;

View File

@@ -31,6 +31,9 @@ export enum UserAvatarColor {
}
export interface UserPreferences {
rating: {
enabled: boolean;
};
memories: {
enabled: boolean;
};
@@ -58,6 +61,9 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
);
return {
rating: {
enabled: false,
},
memories: {
enabled: true,
},

View File

@@ -147,6 +147,7 @@ export interface ISidecarWriteJob extends IEntityJob {
dateTimeOriginal?: string;
latitude?: number;
longitude?: number;
rating?: number;
}
export interface IDeferrableJob extends IEntityJob {

View File

@@ -36,6 +36,7 @@ export interface IStorageRepository {
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
readFile(filepath: string, options?: FileReadOptions<Buffer>): Promise<Buffer>;
writeFile(filepath: string, buffer: Buffer): Promise<void>;
realpath(filepath: string): Promise<string>;
unlink(filepath: string): Promise<void>;
unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
removeEmptyDirs(folder: string, self?: boolean): Promise<void>;

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddRating1722753178937 implements MigrationInterface {
name = 'AddRating1722753178937'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ADD "rating" integer`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "rating"`);
}
}

View File

@@ -58,6 +58,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps"
FROM
"assets" "entity"
@@ -177,6 +178,7 @@ SELECT
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
"AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId",
"AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags",
@@ -628,6 +630,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@@ -769,6 +772,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@@ -886,6 +890,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@@ -1053,6 +1058,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@@ -1129,6 +1135,7 @@ SELECT
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."rating" AS "exifInfo_rating",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",

View File

@@ -322,6 +322,7 @@ FROM
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps"
FROM
"assets" "AssetEntity"

View File

@@ -402,6 +402,7 @@ SELECT
"exif"."profileDescription" AS "exif_profileDescription",
"exif"."colorspace" AS "exif_colorspace",
"exif"."bitsPerSample" AS "exif_bitsPerSample",
"exif"."rating" AS "exif_rating",
"exif"."fps" AS "exif_fps"
FROM
"assets" "asset"

View File

@@ -77,6 +77,7 @@ FROM
"9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."rating" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_rating",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps",
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
@@ -144,6 +145,7 @@ FROM
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."rating" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_rating",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",

View File

@@ -24,6 +24,10 @@ export class StorageRepository implements IStorageRepository {
this.logger.setContext(StorageRepository.name);
}
realpath(filepath: string) {
return fs.realpath(filepath);
}
readdir(folder: string): Promise<string[]> {
return fs.readdir(folder);
}
@@ -52,7 +56,7 @@ export class StorageRepository implements IStorageRepository {
const archive = archiver('zip', { store: true });
const addFile = (input: string, filename: string) => {
archive.file(input, { name: filename });
archive.file(input, { name: filename, mode: 0o644 });
};
const finalize = () => archive.finalize();

View File

@@ -228,6 +228,13 @@ describe(AssetService.name, () => {
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
});
it('should update the exif rating', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
});
});
describe('updateAll', () => {

View File

@@ -158,8 +158,8 @@ export class AssetService {
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
await this.assetRepository.update({ id, ...rest });
const asset = await this.assetRepository.getById(id, {
@@ -405,8 +405,8 @@ export class AssetService {
}
private async updateMetadata(dto: ISidecarWriteJob) {
const { id, description, dateTimeOriginal, latitude, longitude } = dto;
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined);
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
if (Object.keys(writes).length > 0) {
await this.assetRepository.upsertExif({ assetId: id, ...writes });
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });

View File

@@ -2,12 +2,14 @@ import { BadRequestException } from '@nestjs/common';
import { DownloadResponseDto } from 'src/dtos/download.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { DownloadService } from 'src/services/download.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { Readable } from 'typeorm/platform/PlatformTools.js';
import { Mocked, vitest } from 'vitest';
@@ -26,6 +28,7 @@ describe(DownloadService.name, () => {
let sut: DownloadService;
let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let storageMock: Mocked<IStorageRepository>;
it('should work', () => {
@@ -35,9 +38,10 @@ describe(DownloadService.name, () => {
beforeEach(() => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
loggerMock = newLoggerRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new DownloadService(accessMock, assetMock, storageMock);
sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock);
});
describe('downloadArchive', () => {
@@ -109,6 +113,27 @@ describe(DownloadService.name, () => {
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg');
});
it('should resolve symlinks', async () => {
const archiveMock = {
addFile: vitest.fn(),
finalize: vitest.fn(),
stream: new Readable(),
};
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getByIds.mockResolvedValue([
{ ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' },
]);
storageMock.realpath.mockResolvedValue('/path/to/realpath.jpg');
storageMock.createZipStream.mockReturnValue(archiveMock);
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({
stream: archiveMock.stream,
});
expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg');
});
});
describe('getDownloadInfo', () => {

View File

@@ -7,7 +7,8 @@ import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/d
import { AssetEntity } from 'src/entities/asset.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
import { HumanReadableSize } from 'src/utils/bytes';
import { usePagination } from 'src/utils/pagination';
@@ -18,9 +19,11 @@ export class DownloadService {
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.access = AccessCore.create(accessRepository);
this.logger.setContext(DownloadService.name);
}
async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
@@ -83,7 +86,14 @@ export class DownloadService {
filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`;
}
zip.addFile(originalPath, filename);
let realpath = originalPath;
try {
realpath = await this.storageRepository.realpath(originalPath);
} catch {
this.logger.warn('Unable to resolve realpath', { originalPath });
}
zip.addFile(realpath, filename);
}
void zip.finalize();

View File

@@ -606,6 +606,7 @@ describe(MetadataService.name, () => {
ProfileDescription: 'extensive description',
ProjectionType: 'equirectangular',
tz: '+02:00',
Rating: 3,
};
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.readTags.mockResolvedValue(tags);
@@ -638,6 +639,7 @@ describe(MetadataService.name, () => {
profileDescription: tags.ProfileDescription,
projectionType: 'EQUIRECTANGULAR',
timeZone: tags.tz,
rating: tags.Rating,
});
expect(assetMock.update).toHaveBeenCalledWith({
id: assetStub.image.id,

View File

@@ -273,7 +273,7 @@ export class MetadataService implements OnEvents {
}
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
const { id, description, dateTimeOriginal, latitude, longitude } = job;
const { id, description, dateTimeOriginal, latitude, longitude, rating } = job;
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
return JobStatus.FAILED;
@@ -287,6 +287,7 @@ export class MetadataService implements OnEvents {
DateTimeOriginal: dateTimeOriginal,
GPSLatitude: latitude,
GPSLongitude: longitude,
Rating: rating,
},
_.isUndefined,
);
@@ -503,6 +504,7 @@ export class MetadataService implements OnEvents {
profileDescription: tags.ProfileDescription || null,
projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null,
timeZone: tags.tz ?? null,
rating: tags.Rating ?? null,
};
if (exifData.latitude === 0 && exifData.longitude === 0) {

View File

@@ -253,6 +253,7 @@ export const sharedLinkStub = {
bitsPerSample: 8,
colorspace: 'sRGB',
autoStackId: null,
rating: 3,
},
tags: [],
sharedLinks: [],

View File

@@ -56,6 +56,7 @@ export const newStorageRepositoryMock = (reset = true): Mocked<IStorageRepositor
mkdirSync: vitest.fn(),
checkDiskUsage: vitest.fn(),
readdir: vitest.fn(),
realpath: vitest.fn().mockImplementation((filepath: string) => Promise.resolve(filepath)),
stat: vitest.fn(),
crawl: vitest.fn(),
walk: vitest.fn().mockImplementation(async function* () {}),

View File

@@ -32,7 +32,7 @@ export default [
'**/svelte.config.js',
'eslint.config.mjs',
'postcss.config.cjs',
'tailwind.config.cjs',
'tailwind.config.js',
],
},
...compat.extends(

119
web/package-lock.json generated
View File

@@ -79,7 +79,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"typescript": "^5.3.3"
}
},
@@ -2082,9 +2082,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.19",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.19.tgz",
"integrity": "sha512-r/lah3nnYEZX1btlvpSy+Exkt1aWhmOP5pnCt+BBro+tZrh2Zci+26Xnm1fCBLLMeM5q7gHvWiS8c/UtrWjdvQ==",
"version": "2.5.20",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.20.tgz",
"integrity": "sha512-47rJ5BoYwURE/Rp7FNMLp3NzdbWC9DQ/PmKd0mebxT2D/PrPxZxcLImcD3zsWdX2iS6oJk8ITJbO/N2lWnnUqA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -3025,9 +3025,9 @@
"peer": true
},
"node_modules/autoprefixer": {
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
{
@@ -3043,12 +3043,13 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -3107,9 +3108,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"dev": true,
"funding": [
{
@@ -3125,11 +3126,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -3210,9 +3212,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001600",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
"integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"dev": true,
"funding": [
{
@@ -3227,7 +3229,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.1",
@@ -3775,10 +3778,11 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.701",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz",
"integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==",
"dev": true
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
"integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -3943,10 +3947,11 @@
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -5672,9 +5677,10 @@
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
@@ -5986,10 +5992,11 @@
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
@@ -6388,9 +6395,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"dev": true,
"funding": [
{
@@ -8151,12 +8158,13 @@
}
},
"node_modules/svelte-maplibre": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.9.tgz",
"integrity": "sha512-y0NbKGquYCtQQi3vF1M09++Gg8TR5u/4zie1Rb2FIQI8XpvlBJJbBOsY8rkAGjRkH8t2BBtGstCRuoVHzkq3lA==",
"version": "0.9.10",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.10.tgz",
"integrity": "sha512-MYTMogRPzzgXDZGub4ivfdY1/P0uPxZfo/REQhne0zdBLc6cd4n1U4SqY9SoEGNN0CGW1KvSLfc7acx0kxzXlw==",
"license": "MIT",
"dependencies": {
"d3-geo": "^3.1.0",
"dequal": "^2.0.3",
"just-compare": "^2.3.0",
"just-flush": "^2.3.0",
"maplibre-gl": "^4.0.0",
@@ -8264,9 +8272,9 @@
"peer": true
},
"node_modules/tailwindcss": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8682,9 +8690,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"dev": true,
"funding": [
{
@@ -8700,9 +8708,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -8749,14 +8758,14 @@
}
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"rollup": "^4.13.0"
},
"bin": {
@@ -8776,6 +8785,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -8793,6 +8803,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},

View File

@@ -8,7 +8,7 @@
import { handleError } from '$lib/utils/handle-error';
import { getConfig, getConfigDefaults, updateConfig, type SystemConfigDto } from '@immich/sdk';
import { loadConfig } from '$lib/stores/server-config.store';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isEqual } from 'lodash-es';
import { onMount } from 'svelte';
import type { SettingsResetOptions } from './admin-settings';
import { t } from 'svelte-i18n';
@@ -23,12 +23,16 @@
};
export const handleSave = async (update: Partial<SystemConfigDto>) => {
let systemConfigDto = {
...savedConfig,
...update,
};
if (isEqual(systemConfigDto, savedConfig)) {
return;
}
try {
const newConfig = await updateConfig({
systemConfigDto: {
...savedConfig,
...update,
},
systemConfigDto,
});
config = cloneDeep(newConfig);

View File

@@ -26,7 +26,12 @@
<div class="flex flex-col gap-4">
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch title={$t('admin.map_enable_description')} {disabled} bind:checked={config.map.enabled} />
<SettingSwitch
title={$t('admin.map_enable_description')}
subtitle={$t('admin.map_implications')}
{disabled}
bind:checked={config.map.enabled}
/>
<hr />

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