diff --git a/.gitattributes b/.gitattributes
index 32ea167bb..48c4dbdb0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,6 +5,8 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true
+mobile/lib/**/*.g.dart -diff -merge
+mobile/lib/**/*.g.dart linguist-generated=true
cli/src/api/open-api/**/*.md -diff -merge
cli/src/api/open-api/**/*.md linguist-generated=true
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 28b2b7924..0d8bd637d 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,5 +1,5 @@
# These are supported funding model platforms
-github: alextran1502
+github: immich-app
liberapay: alex.tran1502
custom: https://www.buymeacoffee.com/altran1502
diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index 4a69f48a0..085f078c9 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -20,7 +20,7 @@ jobs:
name: Build and sign Android
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
- runs-on: macos-12
+ runs-on: macos-13
steps:
- name: Determine ref
diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml
index 3cfaf0b2f..8b89cba10 100644
--- a/.github/workflows/cache-cleanup.yml
+++ b/.github/workflows/cache-cleanup.yml
@@ -1,4 +1,4 @@
-name: Clean up actions cache on PR close
+name: Cache Cleanup
on:
pull_request:
types:
@@ -10,6 +10,7 @@ concurrency:
jobs:
cleanup:
+ name: Cleanup
runs-on: ubuntu-latest
steps:
- name: Check out code
diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml
new file mode 100644
index 000000000..c66dcfb3c
--- /dev/null
+++ b/.github/workflows/cli-release.yml
@@ -0,0 +1,23 @@
+name: CLI Release
+on:
+ workflow_dispatch:
+
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./cli
+
+ steps:
+ - uses: actions/checkout@v4
+ # Setup .npmrc file to publish to npm
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20.x"
+ registry-url: "https://registry.npmjs.org"
+ - run: npm ci
+ - run: npm publish
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml
index 6de84423f..a49ba5591 100644
--- a/.github/workflows/docker-cleanup.yml
+++ b/.github/workflows/docker-cleanup.yml
@@ -5,7 +5,7 @@
#
# This workflow will not trigger runs on forked repos.
-name: Cleanup Old Docker Images
+name: Docker Cleanup
on:
pull_request:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 4df3ade45..4bebc7502 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -1,4 +1,4 @@
-name: Build and Push Docker Images
+name: Docker
on:
workflow_dispatch:
@@ -18,6 +18,7 @@ permissions:
jobs:
build_and_push:
+ name: Build and Push
runs-on: ubuntu-latest
strategy:
# Prevent a failure in one image from stopping the other builds
@@ -96,7 +97,7 @@ jobs:
fi
- name: Build and push image
- uses: docker/build-push-action@v5.0.0
+ uses: docker/build-push-action@v5.1.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 18343c06f..661287252 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -32,3 +32,8 @@ jobs:
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile
+
+ # Enable after riverpod generator migration is completed
+ # - name: Run dart custom lint
+ # run: dart run custom_lint
+ # working-directory: ./mobile
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 07e9f16d3..a2f946f02 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -11,7 +11,7 @@ concurrency:
jobs:
e2e-tests:
- name: Run end-to-end test suites
+ name: Server (e2e)
runs-on: ubuntu-latest
steps:
@@ -24,7 +24,7 @@ jobs:
run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
doc-tests:
- name: Run documentation checks
+ name: Docs
runs-on: ubuntu-latest
defaults:
run:
@@ -45,8 +45,12 @@ jobs:
run: npm run check
if: ${{ !cancelled() }}
+ - name: Run build
+ run: npm run build
+ if: ${{ !cancelled() }}
+
server-unit-tests:
- name: Run server unit test suites and checks
+ name: Server
runs-on: ubuntu-latest
defaults:
run:
@@ -76,7 +80,7 @@ jobs:
if: ${{ !cancelled() }}
cli-unit-tests:
- name: Run cli test suites
+ name: CLI
runs-on: ubuntu-latest
defaults:
run:
@@ -97,12 +101,16 @@ jobs:
run: npm run format
if: ${{ !cancelled() }}
+ - name: Run tsc
+ run: npm run check
+ if: ${{ !cancelled() }}
+
- name: Run unit tests & coverage
run: npm run test:cov
if: ${{ !cancelled() }}
web-unit-tests:
- name: Run web unit test suites and checks
+ name: Web
runs-on: ubuntu-latest
defaults:
run:
@@ -136,7 +144,7 @@ jobs:
# if: ${{ !cancelled() }}
mobile-unit-tests:
- name: Run mobile unit tests
+ name: Mobile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -150,7 +158,7 @@ jobs:
run: flutter test -j 1
ml-unit-tests:
- name: Run ML unit tests and checks
+ name: Machine Learning
runs-on: ubuntu-latest
defaults:
run:
@@ -180,7 +188,7 @@ jobs:
poetry run pytest --cov app
generated-api-up-to-date:
- name: Check generated files are up-to-date
+ name: OpenAPI Clients
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -201,11 +209,11 @@ jobs:
exit 1
generated-typeorm-migrations-up-to-date:
- name: Check generated TypeORM migrations are up-to-date
+ name: TypeORM Migrations
runs-on: ubuntu-latest
services:
postgres:
- image: postgres
+ image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
diff --git a/README.md b/README.md
index cb2dc86e1..7c884ef97 100644
--- a/README.md
+++ b/README.md
@@ -18,14 +18,16 @@
- 中文
- Türkçe
Català
Español
Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## Disclaimer
diff --git a/README_ca_ES.md b/README_ca_ES.md
index d313bd92e..d15c0fee9 100644
--- a/README_ca_ES.md
+++ b/README_ca_ES.md
@@ -19,13 +19,15 @@
English
- 中文
- Türkçe
Español
Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## Avís legal
diff --git a/README_de_DE.md b/README_de_DE.md
new file mode 100644
index 000000000..de0d1e81f
--- /dev/null
+++ b/README_de_DE.md
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Immich - Hoch performante, selbst gehostete Backup Lösung für Fotos und Videos
+
+
+
+
+
+
+ English
+ Català
+ Español
+ Français
+ Italiano
+ 日本語
+ 한국어
+ Nederlands
+ Türkçe
+ 中文
+
+
+## Warnung
+
+- ⚠️ Das Projekt befindet sich unter **sehr aktiver** Entwicklung.
+- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
+- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
+- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup Regel für deine wertvollen Fotos und Videos!
+
+## Inhalt
+
+- [Offizielle Dokumentation](https://immich.app/docs)
+- [Roadmap](https://github.com/orgs/immich-app/projects/1)
+- [Demo](#demo)
+- [Funktionen](#funktionen)
+- [Einführung](https://immich.app/docs/overview/introduction)
+- [Installation](https://immich.app/docs/install/requirements)
+- [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project)
+- [Unterstütze das Projekt](#unterstütze-das-projekt)
+
+## Dokumentation
+
+Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://immich.app zu finden.
+
+## Demo
+
+Die Web-Demo kannst du unter https://demo.immich.app finden.
+
+Für die Handy-App kannst du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
+
+```bash title="Demo Credential"
+Die Anmeldedaten
+email: demo@immich.app
+passwort: demo
+```
+
+```
+Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
+```
+
+## Funktionen
+
+| Funktionen | Mobil | Web |
+| ---------------------------------------------------- | ------ | ----- |
+| Fotos & Videos hochladen und ansehen | Ja | Ja |
+| Automatisches Backup wenn die App offen ist | Ja | n. a. |
+| Selektive Auswahl von Alben zum Backup | Ja | n. a. |
+| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
+| Unterstützt mehrere Benutzer | Ja | Ja |
+| Album und geteilte Alben | Ja | Ja |
+| Scrollleiste | Ja | Ja |
+| Unterstützt RAW Formate | Ja | Ja |
+| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
+| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
+| Administrative Funktionen (Nutzerverwaltung) | Nein | Ja |
+| Backup im Hintergrund | Ja | n. a. |
+| Virtuelles Scrollen | Ja | Ja |
+| OAuth Unterstützung | Ja | Ja |
+| API-Schlüssel | n. a. | Ja |
+| LivePhoto/MotionPhoto Backup und Wiedergabe | Ja | Ja |
+| Benutzerdefinierte Speicherstruktur | Ja | Ja |
+| Öffentliches Teilen | Nein | Ja |
+| Archive und Favoriten | Ja | Ja |
+| Globale Karte | Ja | Ja |
+| Teilen mit Partner | Ja | Ja |
+| Gesichtserkennung und Gruppierung | Ja | Ja |
+| Rückblicke (heute vor x Jahren) | Ja | Ja |
+| Offline Unterstützung | Ja | Nein |
+| Schreibgeschützte Gallerie | Ja | Ja |
+| Gestapelte Bilder | Ja | Ja |
+
+## Unterstütze das Projekt
+
+Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung mir zusätzliche Motivation zu geben um weiterzumachen.
+
+Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
+
+Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
+
+### Spenden
+
+- [Monatliche Spende](https://github.com/sponsors/alextran1502) via GitHub Sponsors
+- [Einmalige Spende](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
+
+## Unterstützer
+
+
+
diff --git a/README_es_ES.md b/README_es_ES.md
index b87367771..c7a6f6910 100644
--- a/README_es_ES.md
+++ b/README_es_ES.md
@@ -19,12 +19,15 @@
English
- 中文
- Türkçe
Català
Français
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## Descargo de responsabilidad
diff --git a/README_fr_FR.md b/README_fr_FR.md
index 64e1c330a..93d01edfc 100644
--- a/README_fr_FR.md
+++ b/README_fr_FR.md
@@ -18,14 +18,16 @@
- 中文
- Türkçe
+ English
Català
Español
- Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## Clause de non-responsabilité
diff --git a/README_it_IT.md b/README_it_IT.md
index 022cd8d24..0c7d20d73 100644
--- a/README_it_IT.md
+++ b/README_it_IT.md
@@ -19,13 +19,15 @@
English
- 中文
- Türkçe
Català
Español
Français
- Nederlands
日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## Declino di responsabilità
diff --git a/README_ja_JP.md b/README_ja_JP.md
index 30661068b..557e0e358 100644
--- a/README_ja_JP.md
+++ b/README_ja_JP.md
@@ -18,13 +18,16 @@
- 中文
- Türkçe
+ English
Català
Español
Français
- Nederlands
Italiano
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
## 免責事項
diff --git a/README_ko_KR.md b/README_ko_KR.md
new file mode 100644
index 000000000..a6a49ae3d
--- /dev/null
+++ b/README_ko_KR.md
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Immich - 고성능 자체 호스팅 사진 및 동영상 백업 솔루션
+
+
+
+
+
+
+ English
+ Català
+ Español
+ Français
+ Italiano
+ 日本語
+ Deutsch
+ Nederlands
+ Türkçe
+ 中文
+
+
+## 주의 사항
+
+- ⚠️ 이 프로젝트는 **매우 활발히** 개발 중입니다.
+- ⚠️ 버그 및 잦은 변경 사항이 있을 수 있습니다.
+- ⚠️ **사진과 동영상을 저장하는 유일한 방법으로 사용하지 마세요.**
+- ⚠️ 중요한 사진과 동영상을 위해 항상 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 백업 계획을 따르세요!
+
+## 목차
+
+- [공식 문서](https://immich.app/docs)
+- [로드맵](https://github.com/orgs/immich-app/projects/1)
+- [데모](#demo)
+- [기능](#features)
+- [소개](https://immich.app/docs/overview/introduction)
+- [설치](https://immich.app/docs/install/requirements)
+- [기여 가이드](https://immich.app/docs/overview/support-the-project)
+- [프로젝트 지원](#support-the-project)
+
+## 문서
+
+설치 가이드를 포함한 주요 문서는 https://immich.app 에서 확인할 수 있습니다.
+
+## 데모
+
+https://demo.immich.app 에서 웹 데모를 체험할 수 있습니다.
+
+모바일 앱의 경우 `서버 엔드포인트 URL`에 `https://demo.immich.app`를 입력합니다.
+
+```bash title="Demo Credential"
+자격 증명
+email: demo@immich.app
+password: demo
+```
+
+```
+사양: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
+```
+
+## 기능
+
+| 기능 | 모바일 | 웹 |
+| ------------------------------------ | ----- | ----- |
+| 사진, 동영상 업로드 및 보기 | 예 | 예 |
+| 앱을 열 때 자동으로 백업 | 예 | N/A |
+| 백업용 앨범 선택 | 예 | N/A |
+| 로컬 기기로 사진 및 동영상 다운로드 | 예 | 예 |
+| 다른 사용자 추가 | 예 | 예 |
+| 앨범 및 공유 앨범 | 예 | 예 |
+| 스와이프/드래그 가능한 스크롤 바 | 예 | 예 |
+| RAW 포맷 지원 | 예 | 예 |
+| 메타데이터 보기 (EXIF, 위치) | 예 | 예 |
+| 메타데이터, 사물, 얼굴 및 클립으로 검색 | 예 | 예 |
+| 관리 기능 (사용자 관리) | 아니요 | 예 |
+| 백그라운드 백업 | 예 | N/A |
+| 가상 스크롤 | 예 | 예 |
+| OAuth 지원 | 예 | 예 |
+| API 키 | N/A | 예 |
+| 라이브 포토/모션 포토 백업 및 재생 | 예 | 예 |
+| 사용자 정의 스토리지 구조 | 예 | 예 |
+| 모든 사용자와 공유 | 아니요 | 예 |
+| 아카이브 및 즐겨찾기 | 예 |예|
+| 글로벌 지도 | 예 | 예 |
+| 특정 사용자와 공유 | 예 | 예 |
+| 얼굴 인식 및 클러스터링 | 예 | 예 |
+| 추억 (~년 전) | 예 | 예 |
+| 오프라인 지원 | 예 | 아니요 |
+| 읽기 전용 갤러리 | 예 | 예 |
+| 사진 스택 | 예 | 예 |
+
+## 프로젝트 지원
+
+저는 이 프로젝트에 전념해왔고, 앞으로도 멈추지 않을 것입니다. 문서를 업데이트하고, 새로운 기능을 추가하고, 버그를 수정하려 합니다. 하지만 혼자서는 할 수 없습니다. 계속해서 나아갈 수 있는 추가적인 동기부여를 위해 당신의 도움이 필요합니다.
+
+[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 진행자가 말했듯이, 우리가 하고 있는 것은 대규모 프로젝트입니다. 언젠가는 이 일을 풀타임으로 하는 것을 희망하며, 이를 실현하기 위해 당신의 도움이 필요합니다.
+
+만약 이에 동의하거나 이 앱을 장기간 사용하고자 한다면, 아래의 수단을 통해 이 프로젝트를 지원해 주세요.
+
+### 후원
+
+- GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/alextran1502)
+- GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
+- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
diff --git a/README_nl_NL.md b/README_nl_NL.md
index cebe69b9e..129d84ebc 100644
--- a/README_nl_NL.md
+++ b/README_nl_NL.md
@@ -18,14 +18,16 @@
- 中文
- Türkçe
+ English
Català
Español
Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Türkçe
+ 中文
## Disclaimer
diff --git a/README_tr_TR.md b/README_tr_TR.md
index 7ef28bdec..c5d221182 100644
--- a/README_tr_TR.md
+++ b/README_tr_TR.md
@@ -19,13 +19,15 @@
English
- 中文
Català
Español
Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ 中文
## Feragatname
diff --git a/README_zh_CN.md b/README_zh_CN.md
index fdd6777d6..16d4248ea 100644
--- a/README_zh_CN.md
+++ b/README_zh_CN.md
@@ -23,16 +23,17 @@
English
- Türkçe
Català
Español
Français
- Nederlands
- 日本語
Italiano
+ 日本語
+ 한국어
+ Deutsch
+ Nederlands
+ Türkçe
-
## 免责声明
- ⚠️ 本项目正在 **非常活跃** 地开发中。
diff --git a/cli/.npmignore b/cli/.npmignore
new file mode 100644
index 000000000..e001747ce
--- /dev/null
+++ b/cli/.npmignore
@@ -0,0 +1,10 @@
+**/*.spec.js
+.editorconfig
+.eslintignore
+.eslintrc.js
+.prettierignore
+.prettierrc
+package-lock.json
+testSetup.js
+tsconfig.json
+tsconfig.build.json
diff --git a/cli/LICENSE b/cli/LICENSE
new file mode 100644
index 000000000..a72f39880
--- /dev/null
+++ b/cli/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Hau Tran
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/cli/README.md b/cli/README.md
index a11ade33b..a570a5523 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -1,46 +1,19 @@
-A command-line interface for interfacing with Immich
+A command-line interface for interfacing with the self-hosted photo manager [Immich](https://immich.app/).
-# Getting started
+Please see the [Immich CLI documentation](https://immich.app/docs/features/command-line-interface).
- $ ts-node cli/src
+# For developers
-To start using the CLI, you need to login with an API key first:
+To run the Immich CLI from source, run the following in the cli folder:
- $ ts-node cli/src login-key https://your-immich-instance/api your-api-key
+ $ npm run build
+ $ ts-node .
-NOTE: This will store your api key under ~/.config/immich/auth.yml
+You'll need ts-node, the easiest way to install it is to use npm:
-Next, you can run commands:
+ $ npm i -g ts-node
- $ ts-node cli/src server-info
+You can also build and install the CLI using
-When you're done, log out to remove the credentials from your filesystem
-
- $ ts-node cli/src logout
-
-# Usage
-
-```
-Usage: immich [options] [command]
-
-Immich command line interface
-
-Options:
- -h, --help display help for command
-
-Commands:
- upload [options] [paths...] Upload assets
- import [options] [paths...] Import existing assets
- server-info Display server information
- login-key [instanceUrl] [apiKey] Login using an API key
- help [command] display help for command
-```
-
-# Todo
-
-- Sidecar should check both .jpg.xmp and .xmp
-- Sidecar check could be case-insensitive
-
-# Known issues
-
-- Upload can't use sdk due to multiple issues
+ $ npm run build
+ $ npm install -g .
diff --git a/cli/package-lock.json b/cli/package-lock.json
index a90a6d7ee..e51f03cfa 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -1,21 +1,25 @@
{
- "name": "immich-cli",
+ "name": "@immich/cli",
+ "version": "2.0.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "immich-cli",
+ "name": "@immich/cli",
+ "version": "2.0.4",
+ "license": "MIT",
"dependencies": {
- "axios": "^1.4.0",
+ "axios": "^1.6.2",
"byte-size": "^8.1.1",
"cli-progress": "^3.12.0",
"commander": "^11.0.0",
"form-data": "^4.0.0",
"glob": "^10.3.1",
- "picomatch": "^2.3.1",
- "systeminformation": "^5.18.4",
"yaml": "^2.3.1"
},
+ "bin": {
+ "immich": "dist/src/index.js"
+ },
"devDependencies": {
"@types/byte-size": "^8.1.0",
"@types/chai": "^4.3.5",
@@ -25,14 +29,14 @@
"@types/mime-types": "^2.1.1",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1",
- "@typescript-eslint/eslint-plugin": "^5.60.1",
- "@typescript-eslint/parser": "^5.48.1",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
"chai": "^4.3.7",
"eslint": "^8.43.0",
- "eslint-config-prettier": "^8.8.0",
+ "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2",
- "eslint-plugin-prettier": "^4.2.1",
- "eslint-plugin-unicorn": "^47.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "eslint-plugin-unicorn": "^49.0.0",
"jest": "^29.5.0",
"jest-extended": "^4.0.0",
"jest-message-util": "^29.5.0",
@@ -42,7 +46,7 @@
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.3",
- "typescript": "^4.9.4"
+ "typescript": "^5.0.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -798,9 +802,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
- "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
+ "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1399,6 +1403,26 @@
"node": ">=14"
}
},
+ "node_modules/@pkgr/utils": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
+ "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.3.0",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -1495,9 +1519,9 @@
"dev": true
},
"node_modules/@types/chai": {
- "version": "4.3.10",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
- "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
+ "version": "4.3.11",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz",
+ "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
"dev": true
},
"node_modules/@types/cli-progress": {
@@ -1543,9 +1567,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.8",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
- "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
+ "version": "29.5.10",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz",
+ "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -1580,9 +1604,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
- "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
+ "version": "20.10.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
+ "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1622,32 +1646,33 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+ "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
"dev": true,
"dependencies": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/type-utils": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1655,26 +1680,126 @@
}
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+ "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1682,6 +1807,80 @@
}
}
},
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
@@ -1700,25 +1899,25 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+ "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "*"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1726,6 +1925,105 @@
}
}
},
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript-eslint/types": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
@@ -1959,9 +2257,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
- "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
+ "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -2089,6 +2387,27 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "dev": true,
+ "dependencies": {
+ "big-integer": "^1.6.44"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2182,6 +2501,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dev": true,
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/byte-size": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
@@ -2528,6 +2862,168 @@
"node": ">=0.10.0"
}
},
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dev": true,
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "dev": true,
+ "dependencies": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/execa": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/default-browser/node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/default-browser/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/default-browser/node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2646,15 +3142,15 @@
}
},
"node_modules/eslint": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
- "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
+ "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
- "@eslint/js": "8.53.0",
+ "@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -2701,9 +3197,9 @@
}
},
"node_modules/eslint-config-prettier": {
- "version": "8.10.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
- "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+ "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@@ -2738,33 +3234,41 @@
}
},
"node_modules/eslint-plugin-prettier": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
- "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz",
+ "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==",
"dev": true,
"dependencies": {
- "prettier-linter-helpers": "^1.0.0"
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
},
"engines": {
- "node": ">=12.0.0"
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier"
},
"peerDependencies": {
- "eslint": ">=7.28.0",
- "prettier": ">=2.0.0"
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-unicorn": {
- "version": "47.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz",
- "integrity": "sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==",
+ "version": "49.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-49.0.0.tgz",
+ "integrity": "sha512-0fHEa/8Pih5cmzFW5L7xMEfUTvI9WKeQtjmKpTUmY+BiFCDxkxrTdnURJOHKykhtwIeyYsxnecbGvDCml++z4Q==",
"dev": true,
"dependencies": {
- "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/helper-validator-identifier": "^7.22.20",
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^3.8.0",
"clean-regexp": "^1.0.0",
@@ -2772,13 +3276,11 @@
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
"jsesc": "^3.0.2",
- "lodash": "^4.17.21",
"pluralize": "^8.0.0",
"read-pkg-up": "^7.0.1",
- "regexp-tree": "^0.1.24",
+ "regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
- "safe-regex": "^2.1.1",
- "semver": "^7.3.8",
+ "semver": "^7.5.4",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -2788,7 +3290,7 @@
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
},
"peerDependencies": {
- "eslint": ">=8.38.0"
+ "eslint": ">=8.52.0"
}
},
"node_modules/eslint-scope": {
@@ -3491,6 +3993,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3529,6 +4046,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3559,6 +4094,33 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-wsl/node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4410,12 +4972,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
- },
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -4582,12 +5138,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
- "node_modules/natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4666,6 +5216,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "dev": true,
+ "dependencies": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -4835,6 +5403,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -5298,6 +5867,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -5321,15 +5905,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/safe-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
- "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
- "dev": true,
- "dependencies": {
- "regexp-tree": "~0.1.1"
- }
- },
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -5621,29 +6196,20 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
- "node_modules/systeminformation": {
- "version": "5.21.17",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.17.tgz",
- "integrity": "sha512-JZYRCbIjk3WuBV59A9/rTla2rROX+aAJ9uo2Z1dI+bjieORcukClN8rlM1zE9NYKpULSbaGc+KKct/870lO0DA==",
- "os": [
- "darwin",
- "linux",
- "win32",
- "freebsd",
- "openbsd",
- "netbsd",
- "sunos",
- "android"
- ],
- "bin": {
- "systeminformation": "lib/cli.js"
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": "^14.18.0 || >=16.0.0"
},
"funding": {
- "type": "Buy me a coffee",
- "url": "https://www.buymeacoffee.com/systeminfo"
+ "url": "https://opencollective.com/unts"
}
},
"node_modules/test-exclude": {
@@ -5686,6 +6252,18 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -5713,6 +6291,18 @@
"node": ">=8.0"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+ "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/ts-jest": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
@@ -5860,16 +6450,16 @@
}
},
"node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/undici-types": {
@@ -5878,6 +6468,15 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -6735,9 +7334,9 @@
}
},
"@eslint/js": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
- "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
+ "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true
},
"@humanwhocodes/config-array": {
@@ -7196,6 +7795,20 @@
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true
},
+ "@pkgr/utils": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
+ "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.3.0",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.6.0"
+ }
+ },
"@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -7292,9 +7905,9 @@
"dev": true
},
"@types/chai": {
- "version": "4.3.10",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
- "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
+ "version": "4.3.11",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz",
+ "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
"dev": true
},
"@types/cli-progress": {
@@ -7340,9 +7953,9 @@
}
},
"@types/jest": {
- "version": "29.5.8",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
- "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
+ "version": "29.5.10",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz",
+ "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==",
"dev": true,
"requires": {
"expect": "^29.0.0",
@@ -7377,9 +7990,9 @@
}
},
"@types/node": {
- "version": "20.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
- "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
+ "version": "20.10.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
+ "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
@@ -7419,33 +8032,136 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+ "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
"dev": true,
"requires": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/type-utils": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/utils": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "semver": "^7.5.4"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ }
}
},
"@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+ "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ }
}
},
"@typescript-eslint/scope-manager": {
@@ -7459,15 +8175,73 @@
}
},
"@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+ "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.0.1"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/utils": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "semver": "^7.5.4"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.13.1",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ }
}
},
"@typescript-eslint/types": {
@@ -7624,9 +8398,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
- "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
+ "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -7729,6 +8503,21 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "dev": true
+ },
+ "bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "dev": true,
+ "requires": {
+ "big-integer": "^1.6.44"
+ }
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -7790,6 +8579,15 @@
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true
},
+ "bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dev": true,
+ "requires": {
+ "run-applescript": "^5.0.0"
+ }
+ },
"byte-size": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
@@ -8032,6 +8830,107 @@
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true
},
+ "default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dev": true,
+ "requires": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "dependencies": {
+ "execa": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ }
+ },
+ "human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true
+ },
+ "mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "requires": {
+ "path-key": "^4.0.0"
+ }
+ },
+ "onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^4.0.0"
+ }
+ },
+ "path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true
+ }
+ }
+ },
+ "default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "dev": true,
+ "requires": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ }
+ },
+ "define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true
+ },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -8117,15 +9016,15 @@
"dev": true
},
"eslint": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
- "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
+ "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
- "@eslint/js": "8.53.0",
+ "@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -8181,9 +9080,9 @@
}
},
"eslint-config-prettier": {
- "version": "8.10.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
- "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+ "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"dev": true,
"requires": {}
},
@@ -8197,21 +9096,22 @@
}
},
"eslint-plugin-prettier": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
- "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz",
+ "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==",
"dev": true,
"requires": {
- "prettier-linter-helpers": "^1.0.0"
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
}
},
"eslint-plugin-unicorn": {
- "version": "47.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz",
- "integrity": "sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==",
+ "version": "49.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-49.0.0.tgz",
+ "integrity": "sha512-0fHEa/8Pih5cmzFW5L7xMEfUTvI9WKeQtjmKpTUmY+BiFCDxkxrTdnURJOHKykhtwIeyYsxnecbGvDCml++z4Q==",
"dev": true,
"requires": {
- "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/helper-validator-identifier": "^7.22.20",
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^3.8.0",
"clean-regexp": "^1.0.0",
@@ -8219,13 +9119,11 @@
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
"jsesc": "^3.0.2",
- "lodash": "^4.17.21",
"pluralize": "^8.0.0",
"read-pkg-up": "^7.0.1",
- "regexp-tree": "^0.1.24",
+ "regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
- "safe-regex": "^2.1.1",
- "semver": "^7.3.8",
+ "semver": "^7.5.4",
"strip-indent": "^3.0.0"
}
},
@@ -8721,6 +9619,12 @@
"has": "^1.0.3"
}
},
+ "is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true
+ },
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -8747,6 +9651,15 @@
"is-extglob": "^2.1.1"
}
},
+ "is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^3.0.0"
+ }
+ },
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -8765,6 +9678,23 @@
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true
},
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^2.0.0"
+ },
+ "dependencies": {
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true
+ }
+ }
+ },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -9413,12 +10343,6 @@
"p-locate": "^5.0.0"
}
},
- "lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
- },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -9552,12 +10476,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
- "natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -9623,6 +10541,18 @@
"mimic-fn": "^2.1.0"
}
},
+ "open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "dev": true,
+ "requires": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ }
+ },
"optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -9742,7 +10672,8 @@
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
},
"pirates": {
"version": "4.0.6",
@@ -10060,6 +10991,15 @@
}
}
},
+ "run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "dev": true,
+ "requires": {
+ "execa": "^5.0.0"
+ }
+ },
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -10069,15 +11009,6 @@
"queue-microtask": "^1.2.2"
}
},
- "safe-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
- "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
- "dev": true,
- "requires": {
- "regexp-tree": "~0.1.1"
- }
- },
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -10299,10 +11230,15 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
- "systeminformation": {
- "version": "5.21.17",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.17.tgz",
- "integrity": "sha512-JZYRCbIjk3WuBV59A9/rTla2rROX+aAJ9uo2Z1dI+bjieORcukClN8rlM1zE9NYKpULSbaGc+KKct/870lO0DA=="
+ "synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "requires": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ }
},
"test-exclude": {
"version": "6.0.0",
@@ -10337,6 +11273,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+ "dev": true
+ },
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -10358,6 +11300,13 @@
"is-number": "^7.0.0"
}
},
+ "ts-api-utils": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+ "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+ "dev": true,
+ "requires": {}
+ },
"ts-jest": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
@@ -10440,9 +11389,9 @@
"dev": true
},
"typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true
},
"undici-types": {
@@ -10451,6 +11400,12 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
+ "untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true
+ },
"update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
diff --git a/cli/package.json b/cli/package.json
index ccb87cca8..a1550f7b9 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,14 +1,23 @@
{
- "name": "immich-cli",
+ "name": "@immich/cli",
+ "version": "2.0.4",
+ "description": "Command Line Interface (CLI) for Immich",
+ "main": "dist/index.js",
+ "bin": {
+ "immich": "./dist/src/index.js"
+ },
+ "license": "MIT",
+ "keywords": [
+ "immich",
+ "cli"
+ ],
"dependencies": {
- "axios": "^1.4.0",
+ "axios": "^1.6.2",
"byte-size": "^8.1.1",
"cli-progress": "^3.12.0",
"commander": "^11.0.0",
"form-data": "^4.0.0",
"glob": "^10.3.1",
- "picomatch": "^2.3.1",
- "systeminformation": "^5.18.4",
"yaml": "^2.3.1"
},
"devDependencies": {
@@ -20,14 +29,14 @@
"@types/mime-types": "^2.1.1",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1",
- "@typescript-eslint/eslint-plugin": "^5.60.1",
- "@typescript-eslint/parser": "^5.48.1",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
"chai": "^4.3.7",
"eslint": "^8.43.0",
- "eslint-config-prettier": "^8.8.0",
+ "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2",
- "eslint-plugin-prettier": "^4.2.1",
- "eslint-plugin-unicorn": "^47.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "eslint-plugin-unicorn": "^49.0.0",
"jest": "^29.5.0",
"jest-extended": "^4.0.0",
"jest-message-util": "^29.5.0",
@@ -37,15 +46,17 @@
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.3",
- "typescript": "^4.9.4"
+ "typescript": "^5.0.0"
},
"scripts": {
"build": "tsc --project tsconfig.build.json",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
- "prepack": "yarn build ",
+ "prepack": "npm run build",
"test": "jest",
"test:cov": "jest --coverage",
- "format": "prettier --check ."
+ "format": "prettier --check .",
+ "format:fix": "prettier --write .",
+ "check": "tsc --noEmit"
},
"jest": {
"clearMocks": true,
@@ -62,7 +73,15 @@
"collectCoverageFrom": [
"/src/**/*.(t|j)s"
],
+ "moduleNameMapper": {
+ "^@api(|/.*)$": "/src/api/$1"
+ },
"coverageDirectory": "./coverage",
"testEnvironment": "node"
+ },
+ "repository": {
+ "type": "git",
+ "url": "github:immich-app/immich",
+ "directory": "cli"
}
}
diff --git a/cli/src/__mocks__/axios.ts b/cli/src/__mocks__/axios.ts
deleted file mode 100644
index e447986d8..000000000
--- a/cli/src/__mocks__/axios.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// ./__mocks__/axios.js
-import mockAxios from 'jest-mock-axios';
-export default mockAxios;
diff --git a/cli/src/api/client.ts b/cli/src/api/client.ts
index 713a6ee57..392e824b6 100644
--- a/cli/src/api/client.ts
+++ b/cli/src/api/client.ts
@@ -11,6 +11,7 @@ import {
UserApi,
} from './open-api';
import { ApiConfiguration } from '../cores/api-configuration';
+import FormData from 'form-data';
export class ImmichApi {
public userApi: UserApi;
@@ -35,6 +36,7 @@ export class ImmichApi {
'x-api-key': apiKey,
},
},
+ formDataCtor: FormData,
});
this.userApi = new UserApi(this.config);
diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts
index 731e40643..d0dd30fe9 100644
--- a/cli/src/api/open-api/api.ts
+++ b/cli/src/api/open-api/api.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.87.0
+ * The version of the OpenAPI document: 1.89.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -1164,22 +1164,6 @@ export interface CheckExistingAssetsResponseDto {
*/
'existingIds': Array;
}
-/**
- *
- * @export
- * @enum {string}
- */
-
-export const CitiesFile = {
- Cities15000: 'cities15000',
- Cities5000: 'cities5000',
- Cities1000: 'cities1000',
- Cities500: 'cities500'
-} as const;
-
-export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
-
-
/**
*
* @export
@@ -3832,12 +3816,6 @@ export interface SystemConfigPasswordLoginDto {
* @interface SystemConfigReverseGeocodingDto
*/
export interface SystemConfigReverseGeocodingDto {
- /**
- *
- * @type {CitiesFile}
- * @memberof SystemConfigReverseGeocodingDto
- */
- 'citiesFileOverride': CitiesFile;
/**
*
* @type {boolean}
@@ -3845,8 +3823,6 @@ export interface SystemConfigReverseGeocodingDto {
*/
'enabled': boolean;
}
-
-
/**
*
* @export
@@ -6808,6 +6784,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ * Get all asset of a device that are in the database, ID only.
+ * @param {string} deviceId
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'deviceId' is not null or undefined
+ assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId)
+ const localVarPath = `/asset/device/{deviceId}`
+ .replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication cookie required
+
+ // authentication api_key required
+ await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+ // authentication bearer required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -7477,9 +7495,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
};
},
/**
- * Get all asset of a device that are in the database, ID only.
+ *
+ * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
* @param {string} deviceId
* @param {*} [options] Override http request option.
+ * @deprecated
* @throws {RequiredError}
*/
getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => {
@@ -8311,6 +8331,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
+ /**
+ * Get all asset of a device that are in the database, ID only.
+ * @param {string} deviceId
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options);
+ return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+ },
/**
* Get a single asset\'s information
* @param {string} id
@@ -8458,9 +8488,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
- * Get all asset of a device that are in the database, ID only.
+ *
+ * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
* @param {string} deviceId
* @param {*} [options] Override http request option.
+ * @deprecated
* @throws {RequiredError}
*/
async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
@@ -8686,6 +8718,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> {
return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
},
+ /**
+ * Get all asset of a device that are in the database, ID only.
+ * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters.
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> {
+ return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath));
+ },
/**
* Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@@ -8792,9 +8833,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
- * Get all asset of a device that are in the database, ID only.
+ *
+ * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
* @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
+ * @deprecated
* @throws {RequiredError}
*/
getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> {
@@ -9030,6 +9073,20 @@ export interface AssetApiGetAllAssetsRequest {
readonly ifNoneMatch?: string
}
+/**
+ * Request parameters for getAllUserAssetsByDeviceId operation in AssetApi.
+ * @export
+ * @interface AssetApiGetAllUserAssetsByDeviceIdRequest
+ */
+export interface AssetApiGetAllUserAssetsByDeviceIdRequest {
+ /**
+ *
+ * @type {string}
+ * @memberof AssetApiGetAllUserAssetsByDeviceId
+ */
+ readonly deviceId: string
+}
+
/**
* Request parameters for getAssetById operation in AssetApi.
* @export
@@ -9974,6 +10031,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
}
+ /**
+ * Get all asset of a device that are in the database, ID only.
+ * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters.
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof AssetApi
+ */
+ public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) {
+ return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));
+ }
+
/**
* Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@@ -10104,9 +10172,11 @@ export class AssetApi extends BaseAPI {
}
/**
- * Get all asset of a device that are in the database, ID only.
+ *
+ * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
* @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
+ * @deprecated
* @throws {RequiredError}
* @memberof AssetApi
*/
diff --git a/cli/src/api/open-api/base.ts b/cli/src/api/open-api/base.ts
index d1b589373..f16265aff 100644
--- a/cli/src/api/open-api/base.ts
+++ b/cli/src/api/open-api/base.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.87.0
+ * The version of the OpenAPI document: 1.89.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/common.ts b/cli/src/api/open-api/common.ts
index 14b681287..c48b923a0 100644
--- a/cli/src/api/open-api/common.ts
+++ b/cli/src/api/open-api/common.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.87.0
+ * The version of the OpenAPI document: 1.89.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/configuration.ts b/cli/src/api/open-api/configuration.ts
index 2ceed4d1a..030a53c76 100644
--- a/cli/src/api/open-api/configuration.ts
+++ b/cli/src/api/open-api/configuration.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.87.0
+ * The version of the OpenAPI document: 1.89.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/index.ts b/cli/src/api/open-api/index.ts
index fb697d6fc..19d849d0c 100644
--- a/cli/src/api/open-api/index.ts
+++ b/cli/src/api/open-api/index.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.87.0
+ * The version of the OpenAPI document: 1.89.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/cli/base-command.ts b/cli/src/cli/base-command.ts
index c2fb8fee9..d47f973ac 100644
--- a/cli/src/cli/base-command.ts
+++ b/cli/src/cli/base-command.ts
@@ -9,7 +9,6 @@ import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
export abstract class BaseCommand {
protected sessionService!: SessionService;
protected immichApi!: ImmichApi;
- protected deviceId!: string;
protected user!: UserResponseDto;
protected serverVersion!: ServerVersionResponseDto;
diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts
index 5024604a1..ec101ac04 100644
--- a/cli/src/commands/server-info.ts
+++ b/cli/src/commands/server-info.ts
@@ -1,15 +1,19 @@
import { BaseCommand } from '../cli/base-command';
export default class ServerInfo extends BaseCommand {
- static description = 'Display server information';
- static enableJsonFlag = true;
-
public async run() {
- console.log('Getting server information');
-
await this.connect();
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
- console.log(versionInfo);
+ console.log(`Server is running version ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
+
+ const { data: supportedmedia } = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
+
+ console.log(`Supported image types: ${supportedmedia.image.map((extension) => extension.replace('.', ''))}`);
+
+ console.log(`Supported video types: ${supportedmedia.video.map((extension) => extension.replace('.', ''))}`);
+
+ const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics();
+ console.log(`Images: ${statistics.images}, Videos: ${statistics.videos}, Total: ${statistics.total}`);
}
}
diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts
index 93cb36ec2..8bd3d24d9 100644
--- a/cli/src/commands/upload.ts
+++ b/cli/src/commands/upload.ts
@@ -1,43 +1,38 @@
-import { BaseCommand } from '../cli/base-command';
-import { CrawledAsset } from '../cores/models/crawled-asset';
-import { CrawlService, UploadService } from '../services';
-import * as si from 'systeminformation';
-import FormData from 'form-data';
+import { Asset } from '../cores/models/asset';
+import { CrawlService } from '../services';
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
import cliProgress from 'cli-progress';
import byteSize from 'byte-size';
+import { BaseCommand } from '../cli/base-command';
+import axios, { AxiosRequestConfig } from 'axios';
+import FormData from 'form-data';
export default class Upload extends BaseCommand {
- private crawlService = new CrawlService();
- private uploadService!: UploadService;
- deviceId!: string;
uploadLength!: number;
- dryRun = false;
public async run(paths: string[], options: UploadOptionsDto): Promise {
await this.connect();
- const uuid = await si.uuid();
- this.deviceId = uuid.os || 'CLI';
- this.uploadService = new UploadService(this.immichApi.apiConfiguration);
+ const deviceId = 'CLI';
- this.dryRun = options.dryRun;
+ const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
+ const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
const crawlOptions = new CrawlOptionsDto();
crawlOptions.pathsToCrawl = paths;
crawlOptions.recursive = options.recursive;
- crawlOptions.excludePatterns = options.excludePatterns;
+ crawlOptions.exclusionPatterns = options.exclusionPatterns;
- const crawledFiles: string[] = await this.crawlService.crawl(crawlOptions);
+ const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
if (crawledFiles.length === 0) {
console.log('No assets found, exiting');
return;
}
- const assetsToUpload = crawledFiles.map((path) => new CrawledAsset(path));
+ const assetsToUpload = crawledFiles.map((path) => new Asset(path, deviceId));
const uploadProgress = new cliProgress.SingleBar(
{
@@ -58,118 +53,108 @@ export default class Upload extends BaseCommand {
totalSize += asset.fileSize;
}
+ const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
+
uploadProgress.start(totalSize, 0);
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
- for (const asset of assetsToUpload) {
- uploadProgress.update({
- filename: asset.path,
- });
+ try {
+ for (const asset of assetsToUpload) {
+ uploadProgress.update({
+ filename: asset.path,
+ });
- try {
- if (options.import) {
- const importData = {
- assetPath: asset.path,
- sidecarPath: asset.sidecarPath,
- deviceAssetId: asset.deviceAssetId,
- deviceId: this.deviceId,
- fileCreatedAt: asset.fileCreatedAt,
- fileModifiedAt: asset.fileModifiedAt,
- isFavorite: false,
- isReadOnly: options.readOnly,
- };
+ let skipUpload = false;
+ if (!options.skipHash) {
+ const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
- if (!this.dryRun) {
- await this.uploadService.import(importData);
- }
- } else {
- await this.uploadAsset(asset, options.skipHash);
+ const checkResponse = await this.immichApi.assetApi.checkBulkUpload({
+ assetBulkUploadCheckDto,
+ });
+
+ skipUpload = checkResponse.data.results[0].action === 'reject';
}
- } catch (error) {
- uploadProgress.stop();
- throw error;
- }
- sizeSoFar += asset.fileSize;
- if (!asset.skipped) {
- totalSizeUploaded += asset.fileSize;
- uploadCounter++;
- }
+ if (!skipUpload) {
+ if (!options.dryRun) {
+ const formData = asset.getUploadFormData();
+ const res = await this.uploadAsset(formData);
- uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
+ if (options.album && asset.albumName) {
+ let album = existingAlbums.find((album) => album.albumName === asset.albumName);
+ if (!album) {
+ const res = await this.immichApi.albumApi.createAlbum({
+ createAlbumDto: { albumName: asset.albumName },
+ });
+ album = res.data;
+ existingAlbums.push(album);
+ }
+
+ await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } });
+ }
+ }
+
+ totalSizeUploaded += asset.fileSize;
+ uploadCounter++;
+ }
+
+ sizeSoFar += asset.fileSize;
+
+ uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
+ }
+ } finally {
+ uploadProgress.stop();
}
- uploadProgress.stop();
-
let messageStart;
- if (this.dryRun) {
- messageStart = 'Would have ';
+ if (options.dryRun) {
+ messageStart = 'Would have';
} else {
- messageStart = 'Successfully ';
+ messageStart = 'Successfully';
}
- if (options.import) {
- console.log(`${messageStart} imported ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
+ if (uploadCounter === 0) {
+ console.log('All assets were already uploaded, nothing to do.');
} else {
- if (uploadCounter === 0) {
- console.log('All assets were already uploaded, nothing to do.');
+ console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
+ }
+ if (options.delete) {
+ if (options.dryRun) {
+ console.log(`Would now have deleted assets, but skipped due to dry run`);
} else {
- console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
- }
- if (options.delete) {
- if (this.dryRun) {
- console.log(`Would now have deleted assets, but skipped due to dry run`);
- } else {
- console.log('Deleting assets that have been uploaded...');
- const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
- deletionProgress.start(crawledFiles.length, 0);
+ console.log('Deleting assets that have been uploaded...');
+ const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
+ deletionProgress.start(crawledFiles.length, 0);
- for (const asset of assetsToUpload) {
- if (!this.dryRun) {
- await asset.delete();
- }
- deletionProgress.increment();
+ for (const asset of assetsToUpload) {
+ if (!options.dryRun) {
+ await asset.delete();
}
- deletionProgress.stop();
- console.log('Deletion complete');
+ deletionProgress.increment();
}
+ deletionProgress.stop();
+ console.log('Deletion complete');
}
}
}
- private async uploadAsset(asset: CrawledAsset, skipHash = false) {
- await asset.readData();
+ private async uploadAsset(data: FormData): Promise {
+ const url = this.immichApi.apiConfiguration.instanceUrl + '/asset/upload';
- let skipUpload = false;
- if (!skipHash) {
- const checksum = await asset.hash();
+ const config: AxiosRequestConfig = {
+ method: 'post',
+ maxRedirects: 0,
+ url,
+ headers: {
+ 'x-api-key': this.immichApi.apiConfiguration.apiKey,
+ ...data.getHeaders(),
+ },
+ maxContentLength: Infinity,
+ maxBodyLength: Infinity,
+ data,
+ };
- const checkResponse = await this.uploadService.checkIfAssetAlreadyExists(asset.path, checksum);
- skipUpload = checkResponse.data.results[0].action === 'reject';
- }
-
- if (skipUpload) {
- asset.skipped = true;
- } else {
- const uploadFormData = new FormData();
-
- uploadFormData.append('deviceAssetId', asset.deviceAssetId);
- uploadFormData.append('deviceId', this.deviceId);
- uploadFormData.append('fileCreatedAt', asset.fileCreatedAt);
- uploadFormData.append('fileModifiedAt', asset.fileModifiedAt);
- uploadFormData.append('isFavorite', String(false));
- uploadFormData.append('assetData', asset.assetData, { filename: asset.path });
-
- if (asset.sidecarData) {
- uploadFormData.append('sidecarData', asset.sidecarData, {
- filename: asset.sidecarPath,
- contentType: 'application/xml',
- });
- }
-
- if (!this.dryRun) {
- await this.uploadService.upload(uploadFormData);
- }
- }
+ const res = await axios(config);
+ return res;
}
}
diff --git a/cli/src/cores/constants.ts b/cli/src/cores/constants.ts
deleted file mode 100644
index fd9659efd..000000000
--- a/cli/src/cores/constants.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// Check asset-upload.config.spec.ts for complete list
-// TODO: we should get this list from the server via API in the future
-
-// Videos
-const videos = ['mp4', 'webm', 'mov', '3gp', 'avi', 'm2ts', 'mts', 'mpg', 'flv', 'mkv', 'wmv'];
-
-// Images
-const heic = ['heic', 'heif'];
-const jpeg = ['jpg', 'jpeg'];
-const png = ['png'];
-const gif = ['gif'];
-const tiff = ['tif', 'tiff'];
-const webp = ['webp'];
-const dng = ['dng'];
-const other = [
- '3fr',
- 'ari',
- 'arw',
- 'avif',
- 'cap',
- 'cin',
- 'cr2',
- 'cr3',
- 'crw',
- 'dcr',
- 'nef',
- 'erf',
- 'fff',
- 'iiq',
- 'jxl',
- 'k25',
- 'kdc',
- 'mrw',
- 'orf',
- 'ori',
- 'pef',
- 'psd',
- 'raf',
- 'raw',
- 'rwl',
- 'sr2',
- 'srf',
- 'srw',
- 'orf',
- 'ori',
- 'x3f',
-];
-
-export const ACCEPTED_FILE_EXTENSIONS = [
- ...videos,
- ...jpeg,
- ...png,
- ...heic,
- ...gif,
- ...tiff,
- ...webp,
- ...dng,
- ...other,
-];
diff --git a/cli/src/cores/dto/crawl-options-dto.ts b/cli/src/cores/dto/crawl-options-dto.ts
index f9435d390..bf80b4639 100644
--- a/cli/src/cores/dto/crawl-options-dto.ts
+++ b/cli/src/cores/dto/crawl-options-dto.ts
@@ -1,6 +1,6 @@
export class CrawlOptionsDto {
pathsToCrawl!: string[];
- recursive = false;
- includeHidden = false;
- excludePatterns!: string[];
+ recursive? = false;
+ includeHidden? = false;
+ exclusionPatterns?: string[];
}
diff --git a/cli/src/cores/dto/upload-options-dto.ts b/cli/src/cores/dto/upload-options-dto.ts
index 62538cc15..b788f9b4f 100644
--- a/cli/src/cores/dto/upload-options-dto.ts
+++ b/cli/src/cores/dto/upload-options-dto.ts
@@ -1,9 +1,9 @@
export class UploadOptionsDto {
recursive = false;
- excludePatterns!: string[];
+ exclusionPatterns!: string[];
dryRun = false;
skipHash = false;
delete = false;
- import = false;
readOnly = true;
+ album = false;
}
diff --git a/cli/src/cores/index.ts b/cli/src/cores/index.ts
index 11d6a4170..e9644dae4 100644
--- a/cli/src/cores/index.ts
+++ b/cli/src/cores/index.ts
@@ -1,2 +1 @@
-export * from './constants';
export * from './models';
diff --git a/cli/src/cores/models/asset.ts b/cli/src/cores/models/asset.ts
new file mode 100644
index 000000000..78f7ddba7
--- /dev/null
+++ b/cli/src/cores/models/asset.ts
@@ -0,0 +1,100 @@
+import * as fs from 'fs';
+import { basename } from 'node:path';
+import crypto from 'crypto';
+import Os from 'os';
+import FormData from 'form-data';
+
+export class Asset {
+ readonly path: string;
+ readonly deviceId!: string;
+
+ assetData?: fs.ReadStream;
+ deviceAssetId?: string;
+ fileCreatedAt?: string;
+ fileModifiedAt?: string;
+ sidecarData?: fs.ReadStream;
+ sidecarPath?: string;
+ fileSize!: number;
+ albumName?: string;
+
+ constructor(path: string, deviceId: string) {
+ this.path = path;
+ this.deviceId = deviceId;
+ }
+
+ async process() {
+ const stats = await fs.promises.stat(this.path);
+ this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
+ this.fileCreatedAt = stats.mtime.toISOString();
+ this.fileModifiedAt = stats.mtime.toISOString();
+ this.fileSize = stats.size;
+ this.albumName = this.extractAlbumName();
+
+ this.assetData = this.getReadStream(this.path);
+
+ // TODO: doesn't xmp replace the file extension? Will need investigation
+ const sideCarPath = `${this.path}.xmp`;
+ try {
+ fs.accessSync(sideCarPath, fs.constants.R_OK);
+ this.sidecarData = this.getReadStream(sideCarPath);
+ } catch (error) {}
+ }
+
+ getUploadFormData(): FormData {
+ if (!this.assetData) throw new Error('Asset data not set');
+ if (!this.deviceAssetId) throw new Error('Device asset id not set');
+ if (!this.fileCreatedAt) throw new Error('File created at not set');
+ if (!this.fileModifiedAt) throw new Error('File modified at not set');
+ if (!this.deviceId) throw new Error('Device id not set');
+
+ const data: any = {
+ assetData: this.assetData as any,
+ deviceAssetId: this.deviceAssetId,
+ deviceId: this.deviceId,
+ fileCreatedAt: this.fileCreatedAt,
+ fileModifiedAt: this.fileModifiedAt,
+ isFavorite: String(false),
+ };
+ const formData = new FormData();
+
+ for (const prop in data) {
+ formData.append(prop, data[prop]);
+ }
+
+ if (this.sidecarData) {
+ formData.append('sidecarData', this.sidecarData);
+ }
+
+ return formData;
+ }
+
+ private getReadStream(path: string): fs.ReadStream {
+ return fs.createReadStream(path);
+ }
+
+ async delete(): Promise {
+ return fs.promises.unlink(this.path);
+ }
+
+ public async hash(): Promise {
+ const sha1 = (filePath: string) => {
+ const hash = crypto.createHash('sha1');
+ return new Promise((resolve, reject) => {
+ const rs = fs.createReadStream(filePath);
+ rs.on('error', reject);
+ rs.on('data', (chunk) => hash.update(chunk));
+ rs.on('end', () => resolve(hash.digest('hex')));
+ });
+ };
+
+ return await sha1(this.path);
+ }
+
+ private extractAlbumName(): string {
+ if (Os.platform() === 'win32') {
+ return this.path.split('\\').slice(-2)[0];
+ } else {
+ return this.path.split('/').slice(-2)[0];
+ }
+ }
+}
diff --git a/cli/src/cores/models/crawled-asset.ts b/cli/src/cores/models/crawled-asset.ts
deleted file mode 100644
index 15eb72f2b..000000000
--- a/cli/src/cores/models/crawled-asset.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import * as fs from 'fs';
-import { basename } from 'node:path';
-import crypto from 'crypto';
-
-export class CrawledAsset {
- public path: string;
-
- public assetData?: fs.ReadStream;
- public deviceAssetId?: string;
- public fileCreatedAt?: string;
- public fileModifiedAt?: string;
- public sidecarData?: Buffer;
- public sidecarPath?: string;
- public fileSize!: number;
- public skipped = false;
-
- constructor(path: string) {
- this.path = path;
- }
-
- async readData() {
- this.assetData = fs.createReadStream(this.path);
- }
-
- async process() {
- const stats = await fs.promises.stat(this.path);
- this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
- this.fileCreatedAt = stats.mtime.toISOString();
- this.fileModifiedAt = stats.mtime.toISOString();
- this.fileSize = stats.size;
-
- // TODO: doesn't xmp replace the file extension? Will need investigation
- const sideCarPath = `${this.path}.xmp`;
- try {
- fs.accessSync(sideCarPath, fs.constants.R_OK);
- this.sidecarData = await fs.promises.readFile(sideCarPath);
- this.sidecarPath = sideCarPath;
- } catch (error) {}
- }
-
- async delete(): Promise {
- return fs.promises.unlink(this.path);
- }
-
- public async hash(): Promise {
- const sha1 = (filePath: string) => {
- const hash = crypto.createHash('sha1');
- return new Promise((resolve, reject) => {
- const rs = fs.createReadStream(filePath);
- rs.on('error', reject);
- rs.on('data', (chunk) => hash.update(chunk));
- rs.on('end', () => resolve(hash.digest('hex')));
- });
- };
-
- return await sha1(this.path);
- }
-}
diff --git a/cli/src/cores/models/index.ts b/cli/src/cores/models/index.ts
index ae0990ceb..ea2719dd8 100644
--- a/cli/src/cores/models/index.ts
+++ b/cli/src/cores/models/index.ts
@@ -1 +1 @@
-export * from './crawled-asset';
+export * from './asset';
diff --git a/cli/src/index.ts b/cli/src/index.ts
index c0bbfe0b3..39c17cafd 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -1,9 +1,13 @@
+#! /usr/bin/env node
+
import { program, Option } from 'commander';
import Upload from './commands/upload';
import ServerInfo from './commands/server-info';
import LoginKey from './commands/login/key';
+import Logout from './commands/logout';
+import { version } from '../package.json';
-program.name('immich').description('Immich command line interface');
+program.name('immich').description('Immich command line interface').version(version);
program
.command('upload')
@@ -12,6 +16,11 @@ program
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS'))
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
+ .addOption(
+ new Option('-a, --album', 'Automatically create albums based on folder name')
+ .env('IMMICH_AUTO_CREATE_ALBUM')
+ .default(false),
+ )
.addOption(
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
.env('IMMICH_DRY_RUN')
@@ -20,33 +29,13 @@ program
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.argument('[paths...]', 'One or more paths to assets to be uploaded')
.action(async (paths, options) => {
- options.excludePatterns = options.ignore;
- await new Upload().run(paths, options);
- });
-
-program
- .command('import')
- .description('Import existing assets')
- .usage('[options] [paths...]')
- .addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
- .addOption(
- new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
- .env('IMMICH_DRY_RUN')
- .default(false),
- )
- .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
- .addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
- .argument('[paths...]', 'One or more paths to assets to be imported')
- .action(async (paths, options) => {
- options.import = true;
- options.excludePatterns = options.ignore;
+ options.exclusionPatterns = options.ignore;
await new Upload().run(paths, options);
});
program
.command('server-info')
.description('Display server information')
-
.action(async () => {
await new ServerInfo().run();
});
@@ -60,4 +49,11 @@ program
await new LoginKey().run(paths, options);
});
+program
+ .command('logout')
+ .description('Remove stored credentials')
+ .action(async () => {
+ await new Logout().run();
+ });
+
program.parse(process.argv);
diff --git a/cli/src/services/crawl.service.spec.ts b/cli/src/services/crawl.service.spec.ts
index 487b703e8..3957f193a 100644
--- a/cli/src/services/crawl.service.spec.ts
+++ b/cli/src/services/crawl.service.spec.ts
@@ -1,235 +1,206 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import { CrawlService } from './crawl.service';
import mockfs from 'mock-fs';
-import { toIncludeSameMembers } from 'jest-extended';
-import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
+import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
+import { CrawlService } from '.';
-const matchers = require('jest-extended');
-expect.extend(matchers);
+interface Test {
+ test: string;
+ options: CrawlOptionsDto;
+ files: Record;
+}
-const crawlService = new CrawlService();
+const cwd = process.cwd();
-describe('CrawlService', () => {
- beforeAll(() => {
- // Write a dummy output before mock-fs to prevent some annoying errors
- console.log();
- });
+const tests: Test[] = [
+ {
+ test: 'should return empty when crawling an empty path list',
+ options: {
+ pathsToCrawl: [],
+ },
+ files: {},
+ },
+ {
+ test: 'should crawl a single path',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ },
+ },
+ {
+ test: 'should exclude by file extension',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ exclusionPatterns: ['**/*.tif'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/image.tif': false,
+ },
+ },
+ {
+ test: 'should exclude by file extension without case sensitivity',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ exclusionPatterns: ['**/*.TIF'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/image.tif': false,
+ },
+ },
+ {
+ test: 'should exclude by folder',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ exclusionPatterns: ['**/raw/**'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/raw/image.jpg': false,
+ '/photos/raw2/image.jpg': true,
+ '/photos/folder/raw/image.jpg': false,
+ '/photos/crawl/image.jpg': true,
+ },
+ },
+ {
+ test: 'should crawl multiple paths',
+ options: {
+ pathsToCrawl: ['/photos/', '/images/', '/albums/'],
+ },
+ files: {
+ '/photos/image1.jpg': true,
+ '/images/image2.jpg': true,
+ '/albums/image3.jpg': true,
+ },
+ },
+ {
+ test: 'should support globbing paths',
+ options: {
+ pathsToCrawl: ['/photos*'],
+ },
+ files: {
+ '/photos1/image1.jpg': true,
+ '/photos2/image2.jpg': true,
+ '/images/image3.jpg': false,
+ },
+ },
+ {
+ test: 'should crawl a single path without trailing slash',
+ options: {
+ pathsToCrawl: ['/photos'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ },
+ },
+ {
+ test: 'should crawl a single path',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/subfolder/image1.jpg': true,
+ '/photos/subfolder/image2.jpg': true,
+ '/image1.jpg': false,
+ },
+ },
+ {
+ test: 'should filter file extensions',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/image.txt': false,
+ '/photos/1': false,
+ },
+ },
+ {
+ test: 'should include photo and video extensions',
+ options: {
+ pathsToCrawl: ['/photos/', '/videos/'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/image.jpeg': true,
+ '/photos/image.heic': true,
+ '/photos/image.heif': true,
+ '/photos/image.png': true,
+ '/photos/image.gif': true,
+ '/photos/image.tif': true,
+ '/photos/image.tiff': true,
+ '/photos/image.webp': true,
+ '/photos/image.dng': true,
+ '/photos/image.nef': true,
+ '/videos/video.mp4': true,
+ '/videos/video.mov': true,
+ '/videos/video.webm': true,
+ },
+ },
+ {
+ test: 'should check file extensions without case sensitivity',
+ options: {
+ pathsToCrawl: ['/photos/'],
+ },
+ files: {
+ '/photos/image.jpg': true,
+ '/photos/image.Jpg': true,
+ '/photos/image.jpG': true,
+ '/photos/image.JPG': true,
+ '/photos/image.jpEg': true,
+ '/photos/image.TIFF': true,
+ '/photos/image.tif': true,
+ '/photos/image.dng': true,
+ '/photos/image.NEF': true,
+ },
+ },
+ {
+ test: 'should normalize the path',
+ options: {
+ pathsToCrawl: ['/photos/1/../2'],
+ },
+ files: {
+ '/photos/1/image.jpg': false,
+ '/photos/2/image.jpg': true,
+ },
+ },
+ {
+ test: 'should return absolute paths',
+ options: {
+ pathsToCrawl: ['photos'],
+ },
+ files: {
+ [`${cwd}/photos/1.jpg`]: true,
+ [`${cwd}/photos/2.jpg`]: true,
+ [`/photos/3.jpg`]: false,
+ },
+ },
+];
- it('should crawl a single directory', async () => {
- mockfs({
- '/photos/image.jpg': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should crawl a single file', async () => {
- mockfs({
- '/photos/image.jpg': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/image.jpg'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should crawl a file and a directory', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/images/photo.jpg': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/image.jpg', '/images/'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/images/photo.jpg']);
- });
-
- it('should exclude by file extension', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/image.tif': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- options.excludePatterns = ['**/*.tif'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should exclude by file extension without case sensitivity', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/image.tif': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- options.excludePatterns = ['**/*.TIF'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should exclude by folder', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/raw/image.jpg': '',
- '/photos/raw2/image.jpg': '',
- '/photos/folder/raw/image.jpg': '',
- '/photos/crawl/image.jpg': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- options.excludePatterns = ['**/raw/**'];
- options.recursive = true;
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/photos/raw2/image.jpg', '/photos/crawl/image.jpg']);
- });
-
- it('should crawl multiple paths', async () => {
- mockfs({
- '/photos/image1.jpg': '',
- '/images/image2.jpg': '',
- '/albums/image3.jpg': '',
- });
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/', '/images/', '/albums/'];
- options.recursive = false;
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image1.jpg', '/images/image2.jpg', '/albums/image3.jpg']);
- });
-
- it('should crawl a single path without trailing slash', async () => {
- mockfs({
- '/photos/image.jpg': '',
- });
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should crawl a single path without recursion', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/subfolder/image1.jpg': '',
- '/photos/subfolder/image2.jpg': '',
- '/image1.jpg': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should crawl a single path with recursion', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/subfolder/image1.jpg': '',
- '/photos/subfolder/image2.jpg': '',
- '/image1.jpg': '',
- });
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- options.recursive = true;
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers([
- '/photos/image.jpg',
- '/photos/subfolder/image1.jpg',
- '/photos/subfolder/image2.jpg',
- ]);
- });
-
- it('should filter file extensions', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/image.txt': '',
- '/photos/1': '',
- });
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
- });
-
- it('should include photo and video extensions', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/image.jpeg': '',
- '/photos/image.heic': '',
- '/photos/image.heif': '',
- '/photos/image.png': '',
- '/photos/image.gif': '',
- '/photos/image.tif': '',
- '/photos/image.tiff': '',
- '/photos/image.webp': '',
- '/photos/image.dng': '',
- '/photos/image.nef': '',
- '/videos/video.mp4': '',
- '/videos/video.mov': '',
- '/videos/video.webm': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/', '/videos/'];
- const paths: string[] = await crawlService.crawl(options);
-
- expect(paths).toIncludeSameMembers([
- '/photos/image.jpg',
- '/photos/image.jpeg',
- '/photos/image.heic',
- '/photos/image.heif',
- '/photos/image.png',
- '/photos/image.gif',
- '/photos/image.tif',
- '/photos/image.tiff',
- '/photos/image.webp',
- '/photos/image.dng',
- '/photos/image.nef',
- '/videos/video.mp4',
- '/videos/video.mov',
- '/videos/video.webm',
- ]);
- });
-
- it('should check file extensions without case sensitivity', async () => {
- mockfs({
- '/photos/image.jpg': '',
- '/photos/image.Jpg': '',
- '/photos/image.jpG': '',
- '/photos/image.JPG': '',
- '/photos/image.jpEg': '',
- '/photos/image.TIFF': '',
- '/photos/image.tif': '',
- '/photos/image.dng': '',
- '/photos/image.NEF': '',
- });
-
- const options = new CrawlOptionsDto();
- options.pathsToCrawl = ['/photos/'];
- const paths: string[] = await crawlService.crawl(options);
- expect(paths).toIncludeSameMembers([
- '/photos/image.jpg',
- '/photos/image.Jpg',
- '/photos/image.jpG',
- '/photos/image.JPG',
- '/photos/image.jpEg',
- '/photos/image.TIFF',
- '/photos/image.tif',
- '/photos/image.dng',
- '/photos/image.NEF',
- ]);
- });
+describe(CrawlService.name, () => {
+ const sut = new CrawlService(
+ ['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
+ ['.mov', '.mp4', '.webm'],
+ );
afterEach(() => {
mockfs.restore();
});
+
+ describe('crawl', () => {
+ for (const { test, options, files } of tests) {
+ it(test, async () => {
+ mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
+
+ const actual = await sut.crawl(options);
+ const expected = Object.entries(files)
+ .filter((entry) => entry[1])
+ .map(([file]) => file);
+
+ expect(actual.sort()).toEqual(expected.sort());
+ });
+ }
+ });
});
diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts
index bb90a8d0a..28d7fb912 100644
--- a/cli/src/services/crawl.service.ts
+++ b/cli/src/services/crawl.service.ts
@@ -1,47 +1,28 @@
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
-import { ACCEPTED_FILE_EXTENSIONS } from '../cores';
import { glob } from 'glob';
-import * as fs from 'fs';
export class CrawlService {
- public async crawl(crawlOptions: CrawlOptionsDto): Promise {
- const pathsToCrawl: string[] = crawlOptions.pathsToCrawl;
+ private readonly extensions!: string[];
- const directories: string[] = [];
- const crawledFiles: string[] = [];
+ constructor(image: string[], video: string[]) {
+ this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
+ }
- for await (const currentPath of pathsToCrawl) {
- const stats = await fs.promises.stat(currentPath);
- if (stats.isFile() || stats.isSymbolicLink()) {
- crawledFiles.push(currentPath);
- } else {
- directories.push(currentPath);
- }
+ crawl(crawlOptions: CrawlOptionsDto): Promise {
+ const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
+ if (!pathsToCrawl) {
+ return Promise.resolve([]);
}
- let searchPattern: string;
- if (directories.length === 1) {
- searchPattern = directories[0];
- } else if (directories.length === 0) {
- return crawledFiles;
- } else {
- searchPattern = '{' + directories.join(',') + '}';
- }
+ const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`;
+ const extensions = `*{${this.extensions}}`;
- if (crawlOptions.recursive) {
- searchPattern = searchPattern + '/**/';
- }
-
- searchPattern = `${searchPattern}/*.{${ACCEPTED_FILE_EXTENSIONS.join(',')}}`;
-
- const globbedFiles = await glob(searchPattern, {
+ return glob(`${base}/**/${extensions}`, {
+ absolute: true,
nocase: true,
nodir: true,
- ignore: crawlOptions.excludePatterns,
+ dot: includeHidden,
+ ignore: exclusionPatterns,
});
-
- const returnedFiles = crawledFiles.concat(globbedFiles);
- returnedFiles.sort();
- return returnedFiles;
}
}
diff --git a/cli/src/services/index.ts b/cli/src/services/index.ts
index 15aecfa27..9f0d5f958 100644
--- a/cli/src/services/index.ts
+++ b/cli/src/services/index.ts
@@ -1,2 +1 @@
-export * from './upload.service';
export * from './crawl.service';
diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts
index fb008b128..d1c9d789c 100644
--- a/cli/src/services/session.service.ts
+++ b/cli/src/services/session.service.ts
@@ -46,7 +46,7 @@ export class SessionService {
// Check if server and api key are valid
const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => {
- throw new LoginError(`Failed to connect to the server: ${error.message}`);
+ throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
});
console.log(`Logged in as ${userInfo.email}`);
@@ -78,7 +78,7 @@ export class SessionService {
private async ping(): Promise {
const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => {
- throw new Error(`Failed to connect to the server: ${error.message}`);
+ throw new Error(`Failed to connect to server ${this.api.apiConfiguration.instanceUrl}: ${error.message}`);
});
if (pingResponse.res !== 'pong') {
diff --git a/cli/src/services/upload.service.spec.ts b/cli/src/services/upload.service.spec.ts
deleted file mode 100644
index d4a0e4d3a..000000000
--- a/cli/src/services/upload.service.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { UploadService } from './upload.service';
-import axios from 'axios';
-import FormData from 'form-data';
-import { ApiConfiguration } from '../cores/api-configuration';
-
-jest.mock('axios', () => jest.fn());
-
-describe('UploadService', () => {
- let uploadService: UploadService;
-
- beforeEach(() => {
- const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
-
- uploadService = new UploadService(apiConfiguration);
- });
-
- it('should call axios', async () => {
- const data = new FormData();
-
- await uploadService.upload(data);
-
- expect(axios).toHaveBeenCalled();
- });
-});
diff --git a/cli/src/services/upload.service.ts b/cli/src/services/upload.service.ts
deleted file mode 100644
index c059b3417..000000000
--- a/cli/src/services/upload.service.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import axios, { AxiosRequestConfig } from 'axios';
-import FormData from 'form-data';
-import { ApiConfiguration } from '../cores/api-configuration';
-
-export class UploadService {
- private readonly uploadConfig: AxiosRequestConfig;
- private readonly checkAssetExistenceConfig: AxiosRequestConfig;
- private readonly importConfig: AxiosRequestConfig;
-
- constructor(apiConfiguration: ApiConfiguration) {
- this.uploadConfig = {
- method: 'post',
- maxRedirects: 0,
- url: `${apiConfiguration.instanceUrl}/asset/upload`,
- headers: {
- 'x-api-key': apiConfiguration.apiKey,
- },
- maxContentLength: Number.POSITIVE_INFINITY,
- maxBodyLength: Number.POSITIVE_INFINITY,
- };
-
- this.importConfig = {
- method: 'post',
- maxRedirects: 0,
- url: `${apiConfiguration.instanceUrl}/asset/import`,
- headers: {
- 'x-api-key': apiConfiguration.apiKey,
- 'Content-Type': 'application/json',
- },
- maxContentLength: Number.POSITIVE_INFINITY,
- maxBodyLength: Number.POSITIVE_INFINITY,
- };
-
- this.checkAssetExistenceConfig = {
- method: 'post',
- maxRedirects: 0,
- url: `${apiConfiguration.instanceUrl}/asset/bulk-upload-check`,
- headers: {
- 'x-api-key': apiConfiguration.apiKey,
- 'Content-Type': 'application/json',
- },
- };
- }
-
- public checkIfAssetAlreadyExists(path: string, checksum: string) {
- this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
-
- // TODO: retry on 500 errors?
- return axios(this.checkAssetExistenceConfig);
- }
-
- public upload(data: FormData) {
- this.uploadConfig.data = data;
-
- // TODO: retry on 500 errors?
- return axios(this.uploadConfig);
- }
-
- public import(data: any) {
- this.importConfig.data = data;
-
- // TODO: retry on 500 errors?
- return axios(this.importConfig);
- }
-}
diff --git a/cli/tsconfig.json b/cli/tsconfig.json
index 0ef386a56..b44be15d1 100644
--- a/cli/tsconfig.json
+++ b/cli/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "module": "commonjs",
+ "module": "Node16",
"strict": true,
"declaration": true,
"removeComments": true,
@@ -8,7 +8,7 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
- "target": "es2017",
+ "target": "es2022",
"moduleResolution": "node16",
"sourceMap": true,
"outDir": "./dist",
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index f98861673..0c9de8d34 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -59,7 +59,7 @@ services:
build:
context: ../web
dockerfile: Dockerfile
- command: npm run dev --host
+ command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000"
env_file:
- .env
ports:
diff --git a/docs/docs/FAQ.md b/docs/docs/FAQ.md
index 07a109d46..50ee2c112 100644
--- a/docs/docs/FAQ.md
+++ b/docs/docs/FAQ.md
@@ -12,9 +12,9 @@ sidebar_position: 7
| ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up |
| ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up in the cloud/server and still available in original on the device |
-### How can I sync an existing directory with Immich's server?
+### Can I add my existing photo library?
-Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
+Yes, with an [external library](/docs/features/libraries.md).
### Why are only photos and not videos being uploaded to Immich?
diff --git a/docs/docs/developer/architecture.md b/docs/docs/developer/architecture.md
index 36e6ea939..0ade08227 100644
--- a/docs/docs/developer/architecture.md
+++ b/docs/docs/developer/architecture.md
@@ -34,7 +34,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses
### CLI
-The CLI is a [TypeScript](https://www.typescriptlang.org/) project that parses command line arguments to programmatically upload/import assets to an Immich server. See [Bulk Upload](/docs/features/bulk-upload.md) for more information about its usage.
+The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information.
## Server
diff --git a/docs/docs/features/bulk-upload.md b/docs/docs/features/bulk-upload.md
deleted file mode 100644
index c072f73ef..000000000
--- a/docs/docs/features/bulk-upload.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Bulk Upload (Using the CLI)
-
-You can use the CLI to upload an existing gallery to the Immich server
-
-[Immich CLI Repository](https://github.com/immich-app/CLI)
-
-:::tip Google Photos Takeout
-If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
-:::
-
-## Requirements
-
-- Node.js 16 or above
-- Npm
-
-## Installation
-
-```bash
-npm i -g immich
-```
-
-Pre-installed on the `immich-server` container and can be easily accessed through
-
-```
-immich
-```
-
-### Options
-
-| Parameter | Description |
-| ---------------- | ------------------------------------------------------------------- |
-| --yes / -y | Assume yes on all interactive prompts |
-| --recursive / -r | Include subfolders |
-| --delete / -da | Delete local assets after upload |
-| --key / -k | User's API key |
-| --server / -s | Immich's server address |
-| --threads / -t | Number of threads to use (Default 5) |
-| --album/ -al | Create albums for assets based on the parent folder or a given name |
-
-## Quick Start
-
-Specify user's credential, Immich's server address and port and the directory you would like to upload videos/photos from.
-
-```
-immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg
-```
-
-By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
-
-```
-immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/
-```
-
-### Obtain the API Key
-
-The API key can be obtained in the user setting panel on the web interface.
-
-![Obtain Api Key](./img/obtain-api-key.png)
-
----
-
-### Run via Docker
-
-You can run the CLI inside of a docker container to avoid needing to install anything.
-
-:::caution Running inside Docker
-Be aware that as this runs inside a container, you need to mount the folder from which you want to import into the container.
-:::
-
-```bash title="Upload current directory"
-cd /DIRECTORY/WITH/IMAGES
-docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
-```
-
-```bash title="Upload target directory"
-docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
-```
-
-```bash title="Create an alias"
-alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest'
-immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
-```
-
-:::tip Internal networking
-If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
-
-1. Find the internal Docker network used by Immich via `docker network ls`.
-2. Adapt the above command to pass the `--network ` argument to `docker run`, substituting `` with the result from step 1.
-3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
-
-```bash title="Upload to internal address"
-docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001
-```
-
-:::
-
-### Run from source
-
-```bash title="Clone Repository"
-git clone https://github.com/immich-app/CLI
-```
-
-```bash title="Install dependencies"
-npm install
-```
-
-```bash title="Build the project"
-npm run build
-```
-
-```bash title="Run the command"
-node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory
-```
diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md
new file mode 100644
index 000000000..4d4a0e16e
--- /dev/null
+++ b/docs/docs/features/command-line-interface.md
@@ -0,0 +1,139 @@
+# The Immich CLI
+
+Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli).
+
+## Features
+
+- Upload photos and videos to Immich
+- Check server version
+
+More features are planned for the future.
+
+:::tip Google Photos Takeout
+If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
+:::
+
+## Requirements
+
+- Node.js 20.0 or above
+- Npm
+
+## Installation
+
+```bash
+npm i -g @immich/cli
+```
+
+NOTE: if you previously installed the legacy CLI, you will need to uninstall it first:
+
+```bash
+npm uninstall -g immich
+```
+
+## Usage
+
+```
+immich
+```
+
+```
+Usage: immich [options] [command]
+
+Immich command line interface
+
+Options:
+ -h, --help display help for command
+
+Commands:
+ upload [options] [paths...] Upload assets
+ server-info Display server information
+ login-key [instanceUrl] [apiKey] Login using an API key
+ logout Remove stored credentials
+ help [command] display help for command
+```
+
+## Commands
+
+The upload command supports the following options:
+
+```
+Usage: immich upload [options] [paths...]
+
+Upload assets
+
+Arguments:
+ paths One or more paths to assets to be uploaded
+
+Options:
+ -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
+ -i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS)
+ -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
+ -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
+ -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
+ --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
+ --help display help for command
+```
+
+Note that the above options can read from environment variables as well.
+
+## Quick Start
+
+You begin by authenticating to your Immich server.
+
+```bash
+immich login-key [instanceUrl] [apiKey]
+```
+
+For instance,
+
+```bash
+immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG
+```
+
+This will store your credentials in a file in your home directory. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
+
+Once you are authenticated, you can upload assets to your Immich server.
+
+```bash
+immich upload file1.jpg file2.jpg
+```
+
+By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
+
+```bash
+immich upload --recursive directory/
+```
+
+If you are unsure what will happen, you can use the `--dry-run` option to see what would happen without actually performing any actions.
+
+```bash
+immich upload --dry-run --recursive directory/
+```
+
+By default, the upload command will hash the files before uploading them. This is to avoid uploading the same file multiple times. If you are sure that the files are unique, you can skip this step by passing the `--skip-hash` option. Note that Immich always performs its own deduplication through hashing, so this is merely a performance consideration. If you have good bandwidth it might be faster to skip hashing.
+
+```bash
+immich upload --skip-hash --recursive directory/
+```
+
+You can automatically create albums based on the folder name by passing the `--album` option. This will automatically create albums for each uploaded asset based on the name of the folder they are in.
+
+```bash
+immich upload --album --recursive directory/
+```
+
+It is possible to skip assets matching a glob pattern by passing the `--ignore` option. See [the library documentation](docs/features/libraries.md) on how to use glob patterns. You can add several exclusion patterns if needed.
+
+```bash
+immich upload --ignore **/Raw/** --recursive directory/
+```
+
+```bash
+immich upload --ignore **/Raw/** **/*.tif --recursive directory/
+```
+
+### Obtain the API Key
+
+The API key can be obtained in the user setting panel on the web interface.
+
+![Obtain Api Key](./img/obtain-api-key.png)
diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx
index bb6333659..d858fc18a 100644
--- a/docs/docs/features/mobile-app.mdx
+++ b/docs/docs/features/mobile-app.mdx
@@ -4,10 +4,6 @@ import MobileAppBackup from '../partials/_mobile-app-backup.md';
# Mobile App
-:::tip
-To upload from other devices, try using the [Bulk Upload CLI](/docs/features/bulk-upload.md).
-:::
-
## Download
diff --git a/docs/docs/guides/docker-help.md b/docs/docs/guides/docker-help.md
index 7ea0f6d67..37fdf88cb 100644
--- a/docs/docs/guides/docker-help.md
+++ b/docs/docs/guides/docker-help.md
@@ -14,8 +14,6 @@ docker exec -it # attach to a container with a c
docker exec -it immich_server sh
docker exec -it immich_microservices sh
docker exec -it immich_machine_learning sh
-docker exec -it immich_web sh
-docker exec -it immich_proxy sh
```
## Logs
@@ -26,8 +24,6 @@ docker logs # see the logs for a specific container (by id
docker logs immich_server
docker logs immich_microservices
docker logs immich_machine_learning
-docker logs immich_web
-docker logs immich_proxy
```
:::tip Follow a log
diff --git a/docs/docs/guides/machine-learning.md b/docs/docs/guides/machine-learning.md
index f16c23a1e..587c080ab 100644
--- a/docs/docs/guides/machine-learning.md
+++ b/docs/docs/guides/machine-learning.md
@@ -2,7 +2,7 @@
To alleviate [performance issues on low-memory systems](/docs/FAQ.md#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):
-- Set `IMMICH_MACHINE_LEARNING_URL` to point to the designated ML system, e.g. `http://workstation:3003`.
+- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system.
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md
new file mode 100644
index 000000000..b72bf5a03
--- /dev/null
+++ b/docs/docs/guides/remote-access.md
@@ -0,0 +1,58 @@
+# Remote Access
+
+This page gives a few pointers on how to access your Immich instance from outside your LAN.
+
+:::danger
+Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks.
+:::
+
+## Option 1: VPN to home network
+
+You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/)
+
+### Pros:
+
+- Simple to set up and very secure.
+- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk.
+- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal.
+
+### Cons:
+
+- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider.
+- VPN software needs to be installed and active on both server-side and client-side.
+- Requires you to open a port on your router to your server.
+
+## Option 2: Tailscale
+
+If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
+
+### Pros
+
+- Minimal configuration needed on server and client sides.
+- You are protected against zero-day vulnerabilities on Immich.
+
+### Cons
+
+- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
+- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
+- Tailscale needs to be installed and running on both server-side and client-side.
+
+## Option 3: Reverse Proxy
+
+A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side.
+
+If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](https://immich.app/docs/administration/reverse-proxy).
+
+You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
+
+A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder.
+
+### Pros
+
+- No additional software needs to be installed client-side
+- If you only need access to the web interface remotely, it is possible to set up access controls that shield you from zero-day vulnerabilities on Immich. [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) has a generous free tier.
+
+### Cons
+
+- Complex configuration
+- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out.
diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md
index 8819cdafd..7bd473eb1 100644
--- a/docs/docs/overview/support-the-project.md
+++ b/docs/docs/overview/support-the-project.md
@@ -12,8 +12,8 @@ If you feel like this is the right cause and the app is something you see yourse
## Donation
-- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
-- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
+- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/immich-app)
+- One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time)
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
diff --git a/docs/package-lock.json b/docs/package-lock.json
index b2ddf2e1e..c550a9deb 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -15,7 +15,7 @@
"@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
- "clsx": "^1.2.1",
+ "clsx": "^2.0.0",
"docusaurus-lunr-search": "^2.3.2",
"docusaurus-preset-openapi": "^0.6.3",
"postcss": "^8.4.25",
@@ -28,7 +28,7 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
- "prettier": "^2.8.8",
+ "prettier": "^3.0.0",
"typescript": "^5.1.6"
},
"engines": {
@@ -2603,6 +2603,14 @@
"react-dom": "^16.8.4 || ^17.0.0"
}
},
+ "node_modules/@docusaurus/theme-classic/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@docusaurus/theme-common": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz",
@@ -2633,6 +2641,14 @@
"react-dom": "^16.8.4 || ^17.0.0"
}
},
+ "node_modules/@docusaurus/theme-common/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@docusaurus/theme-search-algolia": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz",
@@ -2663,6 +2679,14 @@
"react-dom": "^16.8.4 || ^17.0.0"
}
},
+ "node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@docusaurus/theme-translations": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
@@ -4948,9 +4972,9 @@
}
},
"node_modules/clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
@@ -5995,6 +6019,14 @@
"react-dom": "^16.8.4 || ^17"
}
},
+ "node_modules/docusaurus-lunr-search/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/docusaurus-plugin-openapi": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz",
@@ -6025,6 +6057,14 @@
"react-dom": "^16.8.4 || ^17.0.0"
}
},
+ "node_modules/docusaurus-plugin-openapi/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -6098,6 +6138,14 @@
"react-dom": "^16.8.4 || ^17.0.0"
}
},
+ "node_modules/docusaurus-theme-openapi/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -10801,15 +10849,15 @@
}
},
"node_modules/prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
+ "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
- "prettier": "bin-prettier.js"
+ "prettier": "bin/prettier.cjs"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -13566,9 +13614,9 @@
}
},
"node_modules/typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16745,6 +16793,13 @@
"rtlcss": "^3.5.0",
"tslib": "^2.4.0",
"utility-types": "^3.10.0"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ }
}
},
"@docusaurus/theme-common": {
@@ -16768,6 +16823,13 @@
"tslib": "^2.4.0",
"use-sync-external-store": "^1.2.0",
"utility-types": "^3.10.0"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ }
}
},
"@docusaurus/theme-search-algolia": {
@@ -16791,6 +16853,13 @@
"lodash": "^4.17.21",
"tslib": "^2.4.0",
"utility-types": "^3.10.0"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ }
}
},
"@docusaurus/theme-translations": {
@@ -18515,9 +18584,9 @@
}
},
"clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
},
"collapse-white-space": {
"version": "1.0.6",
@@ -19243,6 +19312,13 @@
"to-vfile": "^6.1.0",
"unified": "^9.0.0",
"unist-util-is": "^4.0.2"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ }
}
},
"docusaurus-plugin-openapi": {
@@ -19268,6 +19344,11 @@
"webpack": "^5.73.0"
},
"dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ },
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -19321,6 +19402,13 @@
"react-redux": "^7.2.0",
"redux-devtools-extension": "^2.13.8",
"webpack": "^5.73.0"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ }
}
},
"dom-converter": {
@@ -22663,9 +22751,9 @@
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
},
"prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
+ "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true
},
"pretty-error": {
@@ -24757,9 +24845,9 @@
}
},
"typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ=="
},
"ua-parser-js": {
"version": "1.0.36",
diff --git a/docs/package.json b/docs/package.json
index 19c972fbb..fa0b5b3e6 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -24,7 +24,7 @@
"@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
- "clsx": "^1.2.1",
+ "clsx": "^2.0.0",
"docusaurus-lunr-search": "^2.3.2",
"docusaurus-preset-openapi": "^0.6.3",
"postcss": "^8.4.25",
@@ -37,7 +37,7 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
- "prettier": "^2.8.8",
+ "prettier": "^3.0.0",
"typescript": "^5.1.6"
},
"browserslist": {
diff --git a/docs/src/pages/milestones.tsx b/docs/src/pages/milestones.tsx
index 38dc091a9..6c7e3c585 100644
--- a/docs/src/pages/milestones.tsx
+++ b/docs/src/pages/milestones.tsx
@@ -3,6 +3,7 @@ import {
mdiAndroid,
mdiAppleIos,
mdiArchiveOutline,
+ mdiBash,
mdiBookSearchOutline,
mdiCakeVariant,
mdiCheckAll,
@@ -15,6 +16,7 @@ import {
mdiFile,
mdiFileSearch,
mdiFolder,
+ mdiForum,
mdiHeart,
mdiImage,
mdiImageAlbum,
@@ -41,6 +43,7 @@ import {
mdiText,
mdiThemeLightDark,
mdiTrashCanOutline,
+ mdiVectorCombine,
mdiVideo,
mdiWeb,
} from '@mdi/js';
@@ -49,6 +52,34 @@ import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [
+ {
+ icon: mdiVectorCombine,
+ description:
+ 'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.',
+ title: 'Container consolidation',
+ release: 'v1.88.0',
+ tag: 'v1.88.0',
+ date: new Date(2023, 10, 20),
+ dateType: DateType.RELEASE,
+ },
+ {
+ icon: mdiBash,
+ description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
+ title: 'CLI v2',
+ release: 'v1.88.0',
+ tag: 'v1.88.0',
+ date: new Date(2023, 10, 19),
+ dateType: DateType.RELEASE,
+ },
+ {
+ icon: mdiForum,
+ description: 'Comment a photo or a video in a shared album',
+ title: 'Activity',
+ release: 'v1.84.0',
+ tag: 'v1.84.0',
+ date: new Date(2023, 10, 1),
+ dateType: DateType.RELEASE,
+ },
{
icon: mdiStar,
description: 'Reach 20K Stars on GitHub!',
diff --git a/docs/src/theme/SearchBar/algolia.css b/docs/src/theme/SearchBar/algolia.css
index 8bea784b9..66e696e96 100644
--- a/docs/src/theme/SearchBar/algolia.css
+++ b/docs/src/theme/SearchBar/algolia.css
@@ -61,8 +61,12 @@
.searchbox__input {
display: inline-block;
box-sizing: border-box;
- -webkit-transition: box-shadow 0.4s ease, background 0.4s ease;
- transition: box-shadow 0.4s ease, background 0.4s ease;
+ -webkit-transition:
+ box-shadow 0.4s ease,
+ background 0.4s ease;
+ transition:
+ box-shadow 0.4s ease,
+ background 0.4s ease;
border: 0;
border-radius: 16px;
box-shadow: inset 0 0 0 1px #cccccc;
@@ -243,7 +247,9 @@
}
.algolia-autocomplete .ds-dropdown-menu {
- box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1);
+ box-shadow:
+ 0 1px 0 0 rgba(0, 0, 0, 0.2),
+ 0 2px 3px 0 rgba(0, 0, 0, 0.1);
}
@media (min-width: 601px) {
diff --git a/docs/vercel.json b/docs/vercel.json
index f973f913d..d05820ebe 100644
--- a/docs/vercel.json
+++ b/docs/vercel.json
@@ -12,7 +12,8 @@
{ "source": "/docs/overview/logo-meaning", "destination": "/docs/overview/logo" },
{ "source": "/docs/overview/technology-stack", "destination": "/docs/developer/architecture" },
{ "source": "/docs/usage/automatic-backup", "destination": "/docs/features/automatic-backup" },
- { "source": "/docs/usage/bulk-upload", "destination": "/docs/features/bulk-upload" },
+ { "source": "/docs/usage/bulk-upload", "destination": "/docs/features/command-line-interface" },
+ { "source": "/docs/features/bulk-upload", "destination": "/docs/features/command-line-interface" },
{ "source": "/docs/usage/oauth", "destination": "/docs/administration/oauth" },
{ "source": "/docs/usage/post-installation", "destination": "/docs/install/post-install" },
{ "source": "/docs/usage/update", "destination": "/docs/install/docker-compose#step-4---upgrading" },
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index d43855320..8d7f400d9 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.11-bookworm as builder
+FROM python:3.11-bookworm@sha256:e5a1b0a194a5fbf94f6e350b31c9a508723f9eeb2f9e9e32c3b65df8520a40cc as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
@@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
-FROM python:3.11-slim-bookworm
+FROM python:3.11-slim-bookworm@sha256:1bc6a3e9356d64ea632791653bc71a56340e8741dab66434ab2739ebf6aed29d
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py
index 2f6902760..bf232071b 100644
--- a/machine-learning/app/main.py
+++ b/machine-learning/app/main.py
@@ -24,7 +24,7 @@ from .schemas import (
TextResponse,
)
-MultiPartParser.max_file_size = 2**24 # spools to disk if payload is 16 MiB or larger
+MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger
app = FastAPI()
diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile
index 1c74546bc..7bd04e0c2 100644
--- a/machine-learning/export/Dockerfile
+++ b/machine-learning/export/Dockerfile
@@ -1,4 +1,4 @@
-FROM mambaorg/micromamba:bookworm-slim as builder
+FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \
diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock
index 2b5a11e8b..e9ec397ff 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -585,68 +585,6 @@ files = [
test = ["PyYAML", "mock", "pytest"]
yaml = ["PyYAML"]
-[[package]]
-name = "contourpy"
-version = "1.1.0"
-description = "Python library for calculating contours of 2D quadrilateral grids"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"},
- {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"},
- {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"},
- {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"},
- {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
- {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
- {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
- {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"},
- {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
- {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
- {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
- {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"},
- {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"},
- {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
- {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
- {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
- {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"},
- {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
- {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
- {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
- {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"},
- {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"},
- {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
- {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
- {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
- {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"},
- {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
- {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
- {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
- {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"},
- {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"},
- {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
- {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
- {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
- {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"},
- {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
- {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
- {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
- {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"},
- {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"},
- {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"},
- {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"},
- {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"},
-]
-
-[package.dependencies]
-numpy = ">=1.16"
-
-[package.extras]
-bokeh = ["bokeh", "selenium"]
-docs = ["furo", "sphinx-copybutton"]
-mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"]
-test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
-test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
-
[[package]]
name = "contourpy"
version = "1.1.1"
@@ -2408,35 +2346,35 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
-version = "1.16.1"
+version = "1.16.2"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
- {file = "onnxruntime-1.16.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:28b2c7f444b4119950b69370801cd66067f403d19cbaf2a444735d7c269cce4a"},
- {file = "onnxruntime-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c24e04f33e7899f6aebb03ed51e51d346c1f906b05c5569d58ac9a12d38a2f58"},
- {file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fa93b166f2d97063dc9f33c5118c5729a4a5dd5617296b6dbef42f9047b3e81"},
- {file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042dd9201b3016ee18f8f8bc4609baf11ff34ca1ff489c0a46bcd30919bf883d"},
- {file = "onnxruntime-1.16.1-cp310-cp310-win32.whl", hash = "sha256:c20aa0591f305012f1b21aad607ed96917c86ae7aede4a4dd95824b3d124ceb7"},
- {file = "onnxruntime-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:5581873e578917bea76d6434ee7337e28195d03488dcf72d161d08e9398c6249"},
- {file = "onnxruntime-1.16.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ef8c0c8abf5f309aa1caf35941380839dc5f7a2fa53da533be4a3f254993f120"},
- {file = "onnxruntime-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e680380bea35a137cbc3efd67a17486e96972901192ad3026ee79c8d8fe264f7"},
- {file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e62cc38ce1a669013d0a596d984762dc9c67c56f60ecfeee0d5ad36da5863f6"},
- {file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:025c7a4d57bd2e63b8a0f84ad3df53e419e3df1cc72d63184f2aae807b17c13c"},
- {file = "onnxruntime-1.16.1-cp311-cp311-win32.whl", hash = "sha256:9ad074057fa8d028df248b5668514088cb0937b6ac5954073b7fb9b2891ffc8c"},
- {file = "onnxruntime-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:d5e43a3478bffc01f817ecf826de7b25a2ca1bca8547d70888594ab80a77ad24"},
- {file = "onnxruntime-1.16.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3aef4d70b0930e29a8943eab248cd1565664458d3a62b2276bd11181f28fd0a3"},
- {file = "onnxruntime-1.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55a7b843a57c8ca0c8ff169428137958146081d5d76f1a6dd444c4ffcd37c3c2"},
- {file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c631af1941bf3b5f7d063d24c04aacce8cff0794e157c497e315e89ac5ad7b"},
- {file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671f296c3d5c233f601e97a10ab5a1dd8e65ba35c7b7b0c253332aba9dff330"},
- {file = "onnxruntime-1.16.1-cp38-cp38-win32.whl", hash = "sha256:eb3802305023dd05e16848d4e22b41f8147247894309c0c27122aaa08793b3d2"},
- {file = "onnxruntime-1.16.1-cp38-cp38-win_amd64.whl", hash = "sha256:fecfb07443d09d271b1487f401fbdf1ba0c829af6fd4fe8f6af25f71190e7eb9"},
- {file = "onnxruntime-1.16.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:de3e12094234db6545c67adbf801874b4eb91e9f299bda34c62967ef0050960f"},
- {file = "onnxruntime-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff723c2a5621b5e7103f3be84d5aae1e03a20621e72219dddceae81f65f240af"},
- {file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a7fb3073aaf6b462e3d7fb433320f7700558a8892e5021780522dc4574292a"},
- {file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:963159f1f699b0454cd72fcef3276c8a1aab9389a7b301bcd8e320fb9d9e8597"},
- {file = "onnxruntime-1.16.1-cp39-cp39-win32.whl", hash = "sha256:85771adb75190db9364b25ddec353ebf07635b83eb94b64ed014f1f6d57a3857"},
- {file = "onnxruntime-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:d32d2b30799c1f950123c60ae8390818381fd5f88bdf3627eeca10071c155dc5"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:e19316bb15c29ca0397e78861ee7cdb4db763ac5c53eaa83169bcdcb1149878c"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:773f6d99d1e6a58936a55a4933c66674241dace9ec4bab71664cdfa170a7cd87"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b8df9583a6e874f1983b85a361d22c205c96e926626eb486d3e69d72642f79"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceef600de846997e3ef5f9af956ae87c88d84d6e925c3e9d435ce17ea223568f"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-win32.whl", hash = "sha256:4fed41edb766c6adea6c34f1eb63a344d697fd4625133e5e48f23950bce60803"},
+ {file = "onnxruntime-1.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:9fc410ec220804fb384e7cb4fd68c474d89da11a1b68184db2001d64ba1477a9"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:aa09d8d9d9a4dc2f6647b5135bb540da36e2d78206aaf14140ba73e05928c4f8"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68f8d3347f11fcc6256266c562e4314b8c6da3e30fc275052a2ab693540b17fd"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16217fa87d3482300a91036f9b499c85215a3b495de1ef9a68cbcf3df1a7c548"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6b7046005442fcd09b86647bdc9a85d60c1367cb36ce7f16b942744cf27fe4"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-win32.whl", hash = "sha256:773c231e526f815b8a3f3549d216cd8fed4c9e226e9e16e86af1b69a4bd29b58"},
+ {file = "onnxruntime-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:90e83a93b3d946c4a1d9dcbae286350accb0d80512d7c1b85953a444d19c0058"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:8616f56905775dd8beeae11cf145542fff06c38cd97bfe9afe0c4a66142fc6d5"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9f5e1d5ca5560044896edb2ad79113f863dc7daa804a26787c7b21c2a96d41e7"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97ce538ffb668c4897e7500a586c150a045869876e0234e0611c4e4f428be63"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cadf175baa782599f36586c23f84fe12b02702ceb59be57dbd8eefc6cc13cc4"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-win32.whl", hash = "sha256:0ffd3b8a3039be713476b8783d254564976664c9b51ec70e7fb5d3e2832bf0f0"},
+ {file = "onnxruntime-1.16.2-cp38-cp38-win_amd64.whl", hash = "sha256:e2211f336e83819edbf174dcf56de35b0dcbfc6c92d3b685c8d85fba19bdf97d"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:98a49bda980bcf819f8d9be880e3e7ba8a1df66aa5ce4fc7bb68ba9acf1fc7ad"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f1e90fa0f43e988cd043e5a4b1eb77eda6cbd7523f316d93d36b33ff1ceb91f"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0cbdb7df8078b2e8d9804de948963961eb8c6f417ef35ed243455162a9a065c"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93c1cbd885c5fe0018b982c9dabe3cc3531416a3b50d0958a291605b32fe3ce"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-win32.whl", hash = "sha256:713101b65d74438f380f5ea2475ce4f6026171e6229100e5be2baa92519fca17"},
+ {file = "onnxruntime-1.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:3382934f9d86060b6bacd3eb4633c5ff904be2c99d3a7fb7faf2828381b15928"},
]
[package.dependencies]
@@ -2578,64 +2516,6 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
-[[package]]
-name = "pandas"
-version = "2.1.0"
-description = "Powerful data structures for data analysis, time series, and statistics"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"},
- {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"},
- {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"},
- {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"},
- {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"},
- {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"},
- {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"},
- {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"},
- {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"},
- {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"},
- {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"},
- {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"},
- {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"},
- {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"},
- {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"},
- {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"},
- {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"},
- {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"},
- {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"},
-]
-
-[package.dependencies]
-numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""}
-python-dateutil = ">=2.8.2"
-pytz = ">=2020.1"
-tzdata = ">=2022.1"
-
-[package.extras]
-all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"]
-aws = ["s3fs (>=2022.05.0)"]
-clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"]
-compression = ["zstandard (>=0.17.0)"]
-computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"]
-consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
-excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"]
-feather = ["pyarrow (>=7.0.0)"]
-fss = ["fsspec (>=2022.05.0)"]
-gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"]
-hdf5 = ["tables (>=3.7.0)"]
-html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"]
-mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"]
-output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"]
-parquet = ["pyarrow (>=7.0.0)"]
-performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"]
-plot = ["matplotlib (>=3.6.1)"]
-postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"]
-spss = ["pyreadstat (>=1.1.5)"]
-sql-other = ["SQLAlchemy (>=1.4.36)"]
-test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
-xml = ["lxml (>=4.8.0)"]
-
[[package]]
name = "pandas"
version = "2.1.2"
@@ -4771,5 +4651,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
-python-versions = "^3.11"
-content-hash = "bba5f87aa67bc1d2283a9f4b471ef78e572337f22413870d324e908014410d53"
+python-versions = "~3.11"
+content-hash = "a4c9b3550bb2a67a54b9ab70e700b24fb9eb0b652e90d7dd8ec92abd121ca6e3"
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index cd4acf9be..b7c831429 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,13 +1,13 @@
[tool.poetry]
name = "machine-learning"
-version = "1.87.0"
+version = "1.89.0"
description = ""
authors = ["Hau Tran "]
readme = "README.md"
packages = [{include = "app"}]
[tool.poetry.dependencies]
-python = "^3.11"
+python = "~3.11"
torch = [
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.1.0", source = "pypi"},
{markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.1.0", source = "pytorch-cpu"}
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index 2d5071a4f..230e2a0d7 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -36,6 +36,9 @@ analyzer:
- openapi/
- openapi/test/
- lib/generated_plugin_registrant.dart
+
+plugins:
+ - custom_lint
dart_code_metrics:
metrics:
@@ -46,7 +49,6 @@ dart_code_metrics:
# Common
- avoid-accessing-collections-by-constant-index
- avoid-accessing-other-classes-private-members
- - avoid-async-call-in-sync-function
- avoid-cascade-after-if-null
- avoid-collapsible-if
- avoid-collection-methods-with-unrelated-types
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 551083964..d68edc784 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 111,
- "android.injected.version.name" => "1.87.0",
+ "android.injected.version.code" => 113,
+ "android.injected.version.name" => "1.89.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml
index 721569491..3e8a519ff 100644
--- a/mobile/android/fastlane/report.xml
+++ b/mobile/android/fastlane/report.xml
@@ -5,17 +5,17 @@
-
+
-
+
-
+
diff --git a/mobile/assets/i18n/ca.json b/mobile/assets/i18n/ca.json
index 6bdfb6255..36aad1957 100644
--- a/mobile/assets/i18n/ca.json
+++ b/mobile/assets/i18n/ca.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Afegeix usuaris",
"all_people_page_title": "Persones",
"all_videos_page_title": "Vídeos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No s'ha trobat res arxivat",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Configuració de la memòria cau",
"change_password_form_confirm_password": "Confirma la contrasenya",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json
index 2e8f00fe4..8290c8f8c 100644
--- a/mobile/assets/i18n/cs-CZ.json
+++ b/mobile/assets/i18n/cs-CZ.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Místní úložiště",
"cache_settings_title": "Nastavení vyrovnávací paměti",
"change_password_form_confirm_password": "Potvrďte heslo",
- "change_password_form_description": "Dobrý den, {firstName} {lastName},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.",
+ "change_password_form_description": "Dobrý den, {name},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.",
"change_password_form_new_password": "Nové heslo",
"change_password_form_password_mismatch": "Hesla se neshodují",
"change_password_form_reenter_new_password": "Znovu zadejte nové heslo",
diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json
index 45658bee1..b5b7c7e45 100644
--- a/mobile/assets/i18n/da-DK.json
+++ b/mobile/assets/i18n/da-DK.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Tilføj brugere",
"all_people_page_title": "Personer",
"all_videos_page_title": "Videoer",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Cache-indstillinger",
"change_password_form_confirm_password": "Bekræft kodeord",
- "change_password_form_description": "Hej {firstName} {lastName},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.",
+ "change_password_form_description": "Hej {name},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.",
"change_password_form_new_password": "Nyt kodeord",
"change_password_form_password_mismatch": "Kodeord er ikke ens",
"change_password_form_reenter_new_password": "Gentag nyt kodeord",
diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json
index 0c871afc8..2af7aecc1 100644
--- a/mobile/assets/i18n/de-DE.json
+++ b/mobile/assets/i18n/de-DE.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Lokaler Speicher",
"cache_settings_title": "Zwischenspeicher Einstellungen",
"change_password_form_confirm_password": "Passwort bestätigen",
- "change_password_form_description": "Hallo {firstName} {lastName}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.",
+ "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.",
"change_password_form_new_password": "Neues Passwort",
"change_password_form_password_mismatch": "Passwörter stimmen nicht überein",
"change_password_form_reenter_new_password": "Passwort erneut eingeben",
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 2b14b872a..0a70750cf 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -28,7 +28,7 @@
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -123,7 +123,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
@@ -229,7 +229,7 @@
"login_password_changed_success": "Password updated successfully",
"map_assets_in_bounds": {
"one": "{} photo",
- "many": "{} photos"
+ "other": "{} photos"
},
"map_cannot_get_user_location": "Cannot get user's location",
"map_location_dialog_cancel": "Cancel",
@@ -395,13 +395,35 @@
"shared_link_edit_show_meta": "Show metadata",
"shared_link_edit_submit_button": "Update link",
"shared_link_empty": "You don't have any shared links",
+ "shared_link_error_server_url_fetch": "Cannot fetch the server url",
+ "shared_link_expired": "Expired",
+ "shared_link_expires_days": {
+ "one": "Expires in {} day",
+ "other": "Expires in {} days"
+ },
+ "shared_link_expires_hours": {
+ "one": "Expires in {} hour",
+ "other": "Expires in {} hours"
+ },
+ "shared_link_expires_minutes": {
+ "one": "Expires in {} minute",
+ "other": "Expires in {} minutes"
+ },
+ "shared_link_expires_seconds": {
+ "one": "Expires in {} second",
+ "other": "Expires in {} seconds"
+ },
+ "shared_link_expires_never": "Expires ∞",
+ "shared_link_info_chip_download": "Download",
+ "shared_link_info_chip_metadata": "EXIF",
+ "shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Manage Shared links",
"share_done": "Done",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
- "sharing_silver_appbar_create_shared_album": "Create shared album",
+ "sharing_silver_appbar_create_shared_album": "New shared album",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Share with partner",
"tab_controller_nav_library": "Library",
@@ -443,5 +465,6 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
- "viewer_unstack": "Un-Stack"
+ "viewer_unstack": "Un-Stack",
+ "scaffold_body_error_occured": "Error occured"
}
diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json
index f6b342f33..f32a8b1f8 100644
--- a/mobile/assets/i18n/es-ES.json
+++ b/mobile/assets/i18n/es-ES.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Almacenamiento local",
"cache_settings_title": "Configuración de la caché",
"change_password_form_confirm_password": "Confirmar Contraseña",
- "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
+ "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
"change_password_form_new_password": "Nueva Contraseña",
"change_password_form_password_mismatch": "Las contraseñas no coinciden",
"change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json
index c1d68cf87..d140b60ee 100644
--- a/mobile/assets/i18n/es-MX.json
+++ b/mobile/assets/i18n/es-MX.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Almacenamiento local",
"cache_settings_title": "Configuración de la caché",
"change_password_form_confirm_password": "Confirmar Contraseña",
- "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
+ "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
"change_password_form_new_password": "Nueva Contraseña",
"change_password_form_password_mismatch": "Las contraseñas no coinciden",
"change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json
index 388fe5ea1..0e03e2fd6 100644
--- a/mobile/assets/i18n/es-PE.json
+++ b/mobile/assets/i18n/es-PE.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Almacenamiento local",
"cache_settings_title": "Configuración de la caché",
"change_password_form_confirm_password": "Confirmar Contraseña",
- "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
+ "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.",
"change_password_form_new_password": "Nueva Contraseña",
"change_password_form_password_mismatch": "Las contraseñas no coinciden",
"change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña",
diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json
index 24ea49a9d..1694f5a1f 100644
--- a/mobile/assets/i18n/es-US.json
+++ b/mobile/assets/i18n/es-US.json
@@ -390,6 +390,28 @@
"shared_link_edit_show_meta": "Mostrar metadatos",
"shared_link_edit_submit_button": "Actualizar enlace",
"shared_link_empty": "No tienes ningún enlace compartido",
+ "shared_link_error_server_url_fetch": "No se puede obtener la URL del servidor",
+ "shared_link_expired": "Expirado",
+ "shared_link_expires_days": {
+ "one": "Expira en {} día",
+ "other": "Expira en {} días"
+ },
+ "shared_link_expires_hours": {
+ "one": "Expira en {} hora",
+ "other": "Expira en {} horas"
+ },
+ "shared_link_expires_minutes": {
+ "one": "Expira en {} minuto",
+ "other": "Expira en {} minutos"
+ },
+ "shared_link_expires_seconds": {
+ "one": "Expira en {} segundo",
+ "other": "Expira en {} segundos"
+ },
+ "shared_link_expires_never": "Sin expiración",
+ "shared_link_info_chip_download": "Descargar",
+ "shared_link_info_chip_metadata": "EXIF",
+ "shared_link_info_chip_upload": "Subir",
"shared_link_manage_links": "Administrar enlaces compartidos",
"share_done": "Hecho",
"share_invite": "Invitar al álbum",
diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json
index 898b7fac4..d746f99c1 100644
--- a/mobile/assets/i18n/fi-FI.json
+++ b/mobile/assets/i18n/fi-FI.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Paikallinen tallennustila",
"cache_settings_title": "Välimuistin asetukset",
"change_password_form_confirm_password": "Vahvista salasana",
- "change_password_form_description": "Hei {firstName} {lastName},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.",
+ "change_password_form_description": "Hei {name},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.",
"change_password_form_new_password": "Uusi salasana",
"change_password_form_password_mismatch": "Salasanat eivät täsmää",
"change_password_form_reenter_new_password": "Uusi salasana uudelleen",
diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json
index ee06e82ca..25e2615d9 100644
--- a/mobile/assets/i18n/fr-CA.json
+++ b/mobile/assets/i18n/fr-CA.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Stockage local",
"cache_settings_title": "Paramètres de mise en cache",
"change_password_form_confirm_password": "Confirmez le mot de passe",
- "change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
+ "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
"change_password_form_new_password": "Nouveau mot de passe",
"change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
"change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json
index 84c1ffd6a..22050072d 100644
--- a/mobile/assets/i18n/fr-FR.json
+++ b/mobile/assets/i18n/fr-FR.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Stockage local",
"cache_settings_title": "Paramètres de mise en cache",
"change_password_form_confirm_password": "Confirmez le mot de passe",
- "change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
+ "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
"change_password_form_new_password": "Nouveau mot de passe",
"change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
"change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json
index 7e3015c9f..9c42afb7d 100644
--- a/mobile/assets/i18n/hi-IN.json
+++ b/mobile/assets/i18n/hi-IN.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json
index 0b9ed145f..60bf00d99 100644
--- a/mobile/assets/i18n/hu-HU.json
+++ b/mobile/assets/i18n/hu-HU.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Felhasználók hozzáadása",
"all_people_page_title": "Emberek",
"all_videos_page_title": "Videók",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Nem található archivált média",
diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json
index 77fed857b..d8a372527 100644
--- a/mobile/assets/i18n/it-IT.json
+++ b/mobile/assets/i18n/it-IT.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Aggiungi utenti",
"all_people_page_title": "Persone",
"all_videos_page_title": "Video",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Impostazioni della Cache",
"change_password_form_confirm_password": "Conferma Password ",
- "change_password_form_description": "Ciao {firstName} {lastName},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto",
+ "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto",
"change_password_form_new_password": "Nuova Password",
"change_password_form_password_mismatch": "Le password non coincidono",
"change_password_form_reenter_new_password": "Inserisci ancora la nuova password ",
diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json
index 58c738af6..4fa57d983 100644
--- a/mobile/assets/i18n/ko-KR.json
+++ b/mobile/assets/i18n/ko-KR.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "로컬 저장소",
"cache_settings_title": "캐시 설정",
"change_password_form_confirm_password": "비밀번호 확인",
- "change_password_form_description": "{firstName} {lastName} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.",
+ "change_password_form_description": "{name} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.",
"change_password_form_new_password": "새 비밀번호",
"change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다",
"change_password_form_reenter_new_password": "새 비밀번호 재입력",
diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json
index daf639d46..875044c29 100644
--- a/mobile/assets/i18n/lv-LV.json
+++ b/mobile/assets/i18n/lv-LV.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Pievienot lietotājus",
"all_people_page_title": "Cilvēki",
"all_videos_page_title": "Videoklipi",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Kešdarbes iestatījumi",
"change_password_form_confirm_password": "Apstiprināt Paroli",
- "change_password_form_description": "Sveiki {FirstName} {LastName},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.",
+ "change_password_form_description": "Sveiki {name},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.",
"change_password_form_new_password": "Jauna Parole",
"change_password_form_password_mismatch": "Paroles nesakrīt",
"change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli",
diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json
index 60828faa2..dfbb54d89 100644
--- a/mobile/assets/i18n/mn.json
+++ b/mobile/assets/i18n/mn.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json
index 0f919be54..cd2e9be27 100644
--- a/mobile/assets/i18n/nb-NO.json
+++ b/mobile/assets/i18n/nb-NO.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Lokal lagring",
"cache_settings_title": "Bufringsinnstillinger",
"change_password_form_confirm_password": "Bekreft passord",
- "change_password_form_description": "Hei {firstName} {lastName}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.",
+ "change_password_form_description": "Hei {name}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.",
"change_password_form_new_password": "Nytt passord",
"change_password_form_password_mismatch": "Passordene stemmer ikke",
"change_password_form_reenter_new_password": "Skriv nytt passord igjen",
diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json
index 842460652..eece8bd40 100644
--- a/mobile/assets/i18n/nl-NL.json
+++ b/mobile/assets/i18n/nl-NL.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
"all_people_page_title": "Personen",
"all_videos_page_title": "Video's",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Geen gearchiveerde items gevonden",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Cache-instellingen",
"change_password_form_confirm_password": "Bevestig wachtwoord",
- "change_password_form_description": "Hallo {firstName} {lastName},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
+ "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
"change_password_form_new_password": "Nieuw wachtwoord",
"change_password_form_password_mismatch": "Wachtwoorden komen niet overeen",
"change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in",
diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json
index 53395ff79..0c7cebe24 100644
--- a/mobile/assets/i18n/pl-PL.json
+++ b/mobile/assets/i18n/pl-PL.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Lokalny magazyn",
"cache_settings_title": "Ustawienia Buforowania",
"change_password_form_confirm_password": "Potwierdź Hasło",
- "change_password_form_description": "Cześć {firstName} {lastName},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.",
+ "change_password_form_description": "Cześć {name},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.",
"change_password_form_new_password": "Nowe Hasło",
"change_password_form_password_mismatch": "Hasła nie są zgodne",
"change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło",
diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json
index 62fdb4d70..2767a8200 100644
--- a/mobile/assets/i18n/ru-RU.json
+++ b/mobile/assets/i18n/ru-RU.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Локальное хранилище",
"cache_settings_title": "Настройки кэширования",
"change_password_form_confirm_password": "Подтвердите пароль",
- "change_password_form_description": "Привет {firstName} {lastName},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.",
+ "change_password_form_description": "Привет {name},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.",
"change_password_form_new_password": "Новый пароль",
"change_password_form_password_mismatch": "Пароли не совпадают",
"change_password_form_reenter_new_password": "Повторно введите новый пароль",
diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json
index aa0f230dd..3e4921ffe 100644
--- a/mobile/assets/i18n/sk-SK.json
+++ b/mobile/assets/i18n/sk-SK.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Lokálne úložisko",
"cache_settings_title": "Nastavenia vyrovnávacej pamäte",
"change_password_form_confirm_password": "Potvrďte heslo",
- "change_password_form_description": "Dobrý deň, {firstName} {lastName},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.",
+ "change_password_form_description": "Dobrý deň, {name},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.",
"change_password_form_new_password": "Nové heslo",
"change_password_form_password_mismatch": "Heslá sa nezhodujú",
"change_password_form_reenter_new_password": "Znova zadajte nové heslo",
diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json
index 7e3015c9f..9c42afb7d 100644
--- a/mobile/assets/i18n/sr-Cyrl.json
+++ b/mobile/assets/i18n/sr-Cyrl.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json
index 2cdc37acf..9588a2ede 100644
--- a/mobile/assets/i18n/sr-Latn.json
+++ b/mobile/assets/i18n/sr-Latn.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Dodaj korisnike",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json
index 7e3015c9f..9c42afb7d 100644
--- a/mobile/assets/i18n/sv-FI.json
+++ b/mobile/assets/i18n/sv-FI.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json
index 31e15c643..026219fe5 100644
--- a/mobile/assets/i18n/sv-SE.json
+++ b/mobile/assets/i18n/sv-SE.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Lägg till användare",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "No archived assets found",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Cache Inställningar",
"change_password_form_confirm_password": "Bekräfta lösenord",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "Nytt lösenord",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json
index ab2e93525..0cfad7eff 100644
--- a/mobile/assets/i18n/th-TH.json
+++ b/mobile/assets/i18n/th-TH.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "เพิ่มผู้ใช้งาน",
"all_people_page_title": "ผู้คน",
"all_videos_page_title": "วิดีโอ",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "ไม่พบทรัพยากรในที่เก็บถาวร",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "ตั้งค่าแคช",
"change_password_form_confirm_password": "ยืนยันรหัสผ่าน",
- "change_password_form_description": "สวัสดี {firstName} {lastName},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง",
+ "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง",
"change_password_form_new_password": "รหัสผ่านใหม่",
"change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน",
"change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่",
diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json
index 3e44a505e..d3df92692 100644
--- a/mobile/assets/i18n/uk-UA.json
+++ b/mobile/assets/i18n/uk-UA.json
@@ -27,7 +27,7 @@
"album_viewer_page_share_add_users": "Додати користувачів",
"all_people_page_title": "Люди",
"all_videos_page_title": "Відео",
- "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
+ "app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Немає архівних елементів",
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Налаштування Кешування",
"change_password_form_confirm_password": "Підтвердити пароль",
- "change_password_form_description": "Привіт {firstName} {lastName},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.",
+ "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.",
"change_password_form_new_password": "Новий Пароль",
"change_password_form_password_mismatch": "Паролі не співпадають",
"change_password_form_reenter_new_password": "Повторіть Новий Пароль",
diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json
index 20edf605a..60edb10b4 100644
--- a/mobile/assets/i18n/vi-VN.json
+++ b/mobile/assets/i18n/vi-VN.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "Lưu trữ cục bộ",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
- "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
+ "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json
index 00b3501c4..28591e395 100644
--- a/mobile/assets/i18n/zh-CN.json
+++ b/mobile/assets/i18n/zh-CN.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "本地存储",
"cache_settings_title": "缓存设置",
"change_password_form_confirm_password": "确认密码",
- "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
+ "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
"change_password_form_new_password": "新密码",
"change_password_form_password_mismatch": "密码不匹配",
"change_password_form_reenter_new_password": "重新输入新的密码",
diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json
index 3673afacb..cc8b89a15 100644
--- a/mobile/assets/i18n/zh-Hans.json
+++ b/mobile/assets/i18n/zh-Hans.json
@@ -119,7 +119,7 @@
"cache_settings_tile_title": "本地存储",
"cache_settings_title": "缓存设置",
"change_password_form_confirm_password": "确认密码",
- "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
+ "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。",
"change_password_form_new_password": "新密码",
"change_password_form_password_mismatch": "密码不匹配",
"change_password_form_reenter_new_password": "重新输入新的密码",
diff --git a/mobile/fonts/WorkSans-Black.ttf b/mobile/fonts/WorkSans-Black.ttf
deleted file mode 100644
index f0f61fbde..000000000
Binary files a/mobile/fonts/WorkSans-Black.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans-Bold.ttf b/mobile/fonts/WorkSans-Bold.ttf
deleted file mode 100644
index c30cb0708..000000000
Binary files a/mobile/fonts/WorkSans-Bold.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans-ExtraBold.ttf b/mobile/fonts/WorkSans-ExtraBold.ttf
deleted file mode 100644
index 2d0d46a3a..000000000
Binary files a/mobile/fonts/WorkSans-ExtraBold.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans-Italic.ttf b/mobile/fonts/WorkSans-Italic.ttf
deleted file mode 100644
index f2bf33102..000000000
Binary files a/mobile/fonts/WorkSans-Italic.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans-Medium.ttf b/mobile/fonts/WorkSans-Medium.ttf
deleted file mode 100644
index 1800fe2d8..000000000
Binary files a/mobile/fonts/WorkSans-Medium.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans-SemiBold.ttf b/mobile/fonts/WorkSans-SemiBold.ttf
deleted file mode 100644
index bce808c82..000000000
Binary files a/mobile/fonts/WorkSans-SemiBold.ttf and /dev/null differ
diff --git a/mobile/fonts/WorkSans.ttf b/mobile/fonts/WorkSans.ttf
deleted file mode 100644
index 09829a516..000000000
Binary files a/mobile/fonts/WorkSans.ttf and /dev/null differ
diff --git a/mobile/fonts/overpass/Overpass-Bold.ttf b/mobile/fonts/overpass/Overpass-Bold.ttf
new file mode 100644
index 000000000..0cd2fb107
Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Bold.ttf differ
diff --git a/mobile/fonts/overpass/Overpass-Italic.ttf b/mobile/fonts/overpass/Overpass-Italic.ttf
new file mode 100644
index 000000000..1031a9e57
Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Italic.ttf differ
diff --git a/mobile/fonts/overpass/Overpass-Medium.ttf b/mobile/fonts/overpass/Overpass-Medium.ttf
new file mode 100644
index 000000000..815276fe2
Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Medium.ttf differ
diff --git a/mobile/fonts/overpass/Overpass-Regular.ttf b/mobile/fonts/overpass/Overpass-Regular.ttf
new file mode 100644
index 000000000..b89906141
Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Regular.ttf differ
diff --git a/mobile/fonts/overpass/Overpass-SemiBold.ttf b/mobile/fonts/overpass/Overpass-SemiBold.ttf
new file mode 100644
index 000000000..b8faf9b99
Binary files /dev/null and b/mobile/fonts/overpass/Overpass-SemiBold.ttf differ
diff --git a/mobile/fonts/overpass/OverpassMono.ttf b/mobile/fonts/overpass/OverpassMono.ttf
new file mode 100644
index 000000000..4502d6123
Binary files /dev/null and b/mobile/fonts/overpass/OverpassMono.ttf differ
diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart
index ac0b14ef4..8daa08d70 100644
--- a/mobile/integration_test/test_utils/general_helper.dart
+++ b/mobile/integration_test/test_utils/general_helper.dart
@@ -2,7 +2,9 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart';
// ignore: depend_on_referenced_packages
@@ -40,7 +42,12 @@ class ImmichTestHelper {
await Store.clear();
await db.writeTxn(() => db.clear());
// Load main Widget
- await tester.pumpWidget(app.getMainWidget(db));
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [dbProvider.overrideWithValue(db)],
+ child: const app.MainWidget(),
+ ),
+ );
// Post run tasks
await EasyLocalization.ensureInitialized();
}
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index c6c23d942..75168ce1c 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -169,4 +169,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
-COCOAPODS: 1.12.1
+COCOAPODS: 1.11.3
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 96e8dcbc6..c5c6ed57c 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 127;
+ CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 127;
+ CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 127;
+ CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 18842a62a..2d3c9d1ee 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -54,11 +54,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.87.0
+ 1.88.0
CFBundleSignature
????
CFBundleVersion
- 127
+ 128
FLTEnableImpeller
ITSAppUsesNonExemptEncryption
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index cc358e65e..85b006ab4 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
- version_number: "1.87.0"
+ version_number: "1.89.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml
index f19548008..947c79674 100644
--- a/mobile/ios/fastlane/report.xml
+++ b/mobile/ios/fastlane/report.xml
@@ -5,32 +5,32 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart
index e3f601358..598f95661 100644
--- a/mobile/lib/constants/immich_colors.dart
+++ b/mobile/lib/constants/immich_colors.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-Color immichBackgroundColor = const Color(0xFFf6f8fe);
-Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
-Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);
+const Color immichBackgroundColor = Color(0xFFf6f8fe);
+const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0);
+const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250);
diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart
new file mode 100644
index 000000000..036881f3c
--- /dev/null
+++ b/mobile/lib/extensions/asyncvalue_extensions.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
+import 'package:logging/logging.dart';
+
+extension LogOnError on AsyncValue {
+ static final Logger _asyncErrorLogger = Logger("AsyncValue");
+
+ Widget widgetWhen({
+ bool skipLoadingOnRefresh = true,
+ Widget Function()? onLoading,
+ Widget Function(Object? error, StackTrace? stack)? onError,
+ required Widget Function(T data) onData,
+ }) {
+ if (isLoading) {
+ bool skip = false;
+ if (isRefreshing) {
+ skip = skipLoadingOnRefresh;
+ }
+
+ if (!skip) {
+ return onLoading?.call() ??
+ const Center(
+ child: ImmichLoadingIndicator(),
+ );
+ }
+ }
+
+ if (hasError && !hasValue) {
+ _asyncErrorLogger.severe("Error occured", error, stackTrace);
+ return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody();
+ }
+
+ return onData(requireValue);
+ }
+}
diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart
index b2e6ed472..6151bd1a5 100644
--- a/mobile/lib/extensions/build_context_extensions.dart
+++ b/mobile/lib/extensions/build_context_extensions.dart
@@ -45,7 +45,7 @@ extension ContextHelper on BuildContext {
) =>
AutoRouter.of(this).navigate(route);
-// Auto-Push replace route from the current context
+ // Auto-Push replace route from the current context
Future autoReplace(PageRouteInfo route) =>
AutoRouter.of(this).replace(route);
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 224fe3ef4..a12c43b6c 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
@@ -7,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:timezone/data/latest.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
@@ -28,7 +30,6 @@ import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:immich_mobile/shared/services/local_notification.service.dart';
-import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart';
@@ -43,7 +44,13 @@ void main() async {
await initApp();
await migrateDatabaseIfNeeded(db);
HttpOverrides.global = HttpSSLCertOverride();
- runApp(getMainWidget(db));
+
+ runApp(
+ ProviderScope(
+ overrides: [dbProvider.overrideWithValue(db)],
+ child: const MainWidget(),
+ ),
+ );
}
Future initApp() async {
@@ -103,19 +110,6 @@ Future loadDb() async {
return db;
}
-Widget getMainWidget(Isar db) {
- return EasyLocalization(
- supportedLocales: locales,
- path: translationsPath,
- useFallbackTranslations: true,
- fallbackLocale: locales.first,
- child: ProviderScope(
- overrides: [dbProvider.overrideWithValue(db)],
- child: const ImmichApp(),
- ),
- );
-}
-
class ImmichApp extends ConsumerStatefulWidget {
const ImmichApp({super.key});
@@ -165,10 +159,9 @@ class ImmichAppState extends ConsumerState
// Android 8 does not support transparent app bars
final info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt <= 26) {
- overlayStyle =
- MediaQuery.of(context).platformBrightness == Brightness.light
- ? SystemUiOverlayStyle.light
- : SystemUiOverlayStyle.dark;
+ overlayStyle = context.isDarkTheme
+ ? SystemUiOverlayStyle.dark
+ : SystemUiOverlayStyle.light;
}
}
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
@@ -200,22 +193,33 @@ class ImmichAppState extends ConsumerState
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
- home: Stack(
- children: [
- MaterialApp.router(
- title: 'Immich',
- debugShowCheckedModeBanner: false,
- themeMode: ref.watch(immichThemeProvider),
- darkTheme: immichDarkTheme,
- theme: immichLightTheme,
- routeInformationParser: router.defaultRouteParser(),
- routerDelegate: router.delegate(
- navigatorObservers: () => [TabNavigationObserver(ref: ref)],
- ),
- ),
- const ImmichLoadingOverlay(),
- ],
+ home: MaterialApp.router(
+ title: 'Immich',
+ debugShowCheckedModeBanner: false,
+ themeMode: ref.watch(immichThemeProvider),
+ darkTheme: immichDarkTheme,
+ theme: immichLightTheme,
+ routeInformationParser: router.defaultRouteParser(),
+ routerDelegate: router.delegate(
+ navigatorObservers: () => [TabNavigationObserver(ref: ref)],
+ ),
),
);
}
}
+
+// ignore: prefer-single-widget-per-file
+class MainWidget extends StatelessWidget {
+ const MainWidget({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return EasyLocalization(
+ supportedLocales: locales,
+ path: translationsPath,
+ useFallbackTranslations: true,
+ fallbackLocale: locales.first,
+ child: const ImmichApp(),
+ );
+ }
+}
diff --git a/mobile/lib/modules/activities/views/activities_page.dart b/mobile/lib/modules/activities/views/activities_page.dart
index 1cfd48b5b..f0c68a349 100644
--- a/mobile/lib/modules/activities/views/activities_page.dart
+++ b/mobile/lib/modules/activities/views/activities_page.dart
@@ -4,12 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
@@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget {
width: 40,
height: 30,
decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(4),
+ borderRadius: const BorderRadius.all(Radius.circular(4)),
image: DecorationImage(
image: CachedNetworkImageProvider(
getThumbnailUrlForRemoteId(
@@ -231,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(title: Text(appBarTitle)),
- body: activities.maybeWhen(
- orElse: () {
- return const Center(child: ImmichLoadingIndicator());
- },
- data: (data) {
+ body: activities.widgetWhen(
+ onData: (data) {
final liked = data.firstWhereOrNull(
(a) =>
a.type == ActivityType.like &&
diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
index 650d4da0d..25747177a 100644
--- a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
+++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
@@ -65,7 +65,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
}
ref.invalidate(albumDetailProvider(album.id));
- Navigator.pop(context);
+ context.pop();
}
return Card(
diff --git a/mobile/lib/modules/album/ui/album_action_outlined_button.dart b/mobile/lib/modules/album/ui/album_action_outlined_button.dart
index eebaefc41..768be0e3c 100644
--- a/mobile/lib/modules/album/ui/album_action_outlined_button.dart
+++ b/mobile/lib/modules/album/ui/album_action_outlined_button.dart
@@ -16,7 +16,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(right: 8.0),
+ padding: const EdgeInsets.only(right: 16.0),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
@@ -32,13 +32,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
),
icon: Icon(
iconData,
- size: 15,
+ size: 18,
color: context.primaryColor,
),
label: Text(
labelText,
- style: context.textTheme.labelSmall?.copyWith(
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelMedium?.copyWith(
+ fontWeight: FontWeight.w600,
),
),
onPressed: onPressed,
diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart
index 7d5e42b1c..b295deec5 100644
--- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart
+++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart
@@ -72,17 +72,13 @@ class AlbumThumbnailCard extends StatelessWidget {
.tr(args: ['${album.assetCount}'])
: 'album_thumbnail_card_items'
.tr(args: ['${album.assetCount}']),
- style: TextStyle(
- fontFamily: 'WorkSans',
- fontSize: 12,
- color: isDarkTheme ? Colors.white : Colors.black,
- ),
+ style: context.textTheme.bodyMedium,
),
if (owner != null) const TextSpan(text: ' · '),
if (owner != null)
TextSpan(
text: owner,
- style: context.textTheme.labelSmall,
+ style: context.textTheme.bodyMedium,
),
],
),
@@ -114,11 +110,9 @@ class AlbumThumbnailCard extends StatelessWidget {
width: cardSize,
child: Text(
album.name,
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: isDarkTheme
- ? context.primaryColor
- : Colors.black,
+ style: context.textTheme.bodyMedium?.copyWith(
+ color: context.primaryColor,
+ fontWeight: FontWeight.w500,
),
),
),
diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
index cd0bd2ba6..0e2fc74fb 100644
--- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart
+++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
@@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
Widget build(BuildContext context, WidgetRef ref) {
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
+ final isProcessing = useProcessingOverlay();
final comments = album.shared
? ref.watch(
activityStatisticsStateProvider(
@@ -52,7 +53,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
: 0;
deleteAlbum() async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
final bool success;
if (album.shared) {
@@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
}
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
Future showConfirmationDialog() async {
@@ -89,7 +90,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
),
actions: [
TextButton(
- onPressed: () => Navigator.pop(context, 'Cancel'),
+ onPressed: () => context.pop('Cancel'),
child: Text(
'Cancel',
style: TextStyle(
@@ -100,7 +101,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
),
TextButton(
onPressed: () {
- Navigator.pop(context, 'Confirm');
+ context.pop('Confirm');
deleteAlbum();
},
child: Text(
@@ -122,7 +123,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
}
void onLeaveAlbumPressed() async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
@@ -131,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
context
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
- Navigator.pop(context);
+ context.pop();
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_leave".tr(),
@@ -140,11 +141,11 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
}
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
void onRemoveFromAlbumPressed() async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
@@ -153,12 +154,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
if (isSuccess) {
- Navigator.pop(context);
+ context.pop();
selectionDisabled();
ref.watch(albumProvider.notifier).getAllAlbums();
ref.invalidate(albumDetailProvider(album.id));
} else {
- Navigator.pop(context);
+ context.pop();
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_remove".tr(),
@@ -167,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
}
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
void handleShareAssets(
@@ -198,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
}
void onShareAssetsTo() async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
handleShareAssets(ref, context, selected);
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
buildBottomSheetActions() {
@@ -210,7 +211,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.ios_share_rounded),
title: const Text(
'album_viewer_appbar_share_to',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onShareAssetsTo(),
),
@@ -219,7 +220,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
'album_viewer_appbar_share_remove',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onRemoveFromAlbumPressed(),
)
@@ -232,7 +233,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'album_viewer_appbar_share_delete',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onDeleteAlbumPressed(),
)
@@ -240,7 +241,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'album_viewer_appbar_share_leave',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onLeaveAlbumPressed(),
),
@@ -253,23 +254,23 @@ class AlbumViewerAppbar extends HookConsumerWidget
ListTile(
leading: const Icon(Icons.person_add_alt_rounded),
onTap: () {
- Navigator.pop(context);
+ context.pop();
onAddUsers!(album);
},
title: const Text(
"album_viewer_page_share_add_users",
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
ListTile(
leading: const Icon(Icons.share_rounded),
onTap: () {
context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
- Navigator.pop(context);
+ context.pop();
},
title: const Text(
"control_bottom_app_bar_share",
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
ListTile(
@@ -277,7 +278,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
title: const Text(
"translated_text_options",
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
];
@@ -286,12 +287,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
ListTile(
leading: const Icon(Icons.add_photo_alternate_outlined),
onTap: () {
- Navigator.pop(context);
+ context.pop();
onAddPhotos!(album);
},
title: const Text(
"share_add_photos",
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
];
diff --git a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart
index b73748f4a..1ece28afb 100644
--- a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart
+++ b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart
@@ -44,7 +44,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
focusNode: titleFocusNode,
- style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ style: context.textTheme.headlineMedium,
controller: titleTextEditController,
onTap: () {
FocusScope.of(context).requestFocus(titleFocusNode);
diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart
index cac70df75..6ef773339 100644
--- a/mobile/lib/modules/album/views/album_options_part.dart
+++ b/mobile/lib/modules/album/views/album_options_part.dart
@@ -24,10 +24,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
final owner = album.owner.value;
final userId = ref.watch(authenticationProvider).userId;
final activityEnabled = useState(album.activityEnabled);
+ final isProcessing = useProcessingOverlay();
final isOwner = owner?.id == userId;
void showErrorMessage() {
- Navigator.pop(context);
+ context.pop();
ImmichToast.show(
context: context,
msg: "shared_album_section_people_action_error".tr(),
@@ -37,7 +38,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
}
void leaveAlbum() async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
try {
final isSuccess =
@@ -54,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
showErrorMessage();
}
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
void removeUserFromAlbum(User user) async {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
try {
await ref
@@ -70,8 +71,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
showErrorMessage();
}
- Navigator.pop(context);
- ImmichLoadingOverlayController.appLoader.hide();
+ context.pop();
+ isProcessing.value = false;
}
void handleUserClick(User user) {
@@ -91,7 +92,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
actions = [
ListTile(
leading: const Icon(Icons.person_remove_rounded),
- title: const Text("shared_album_section_people_remove_user").tr(),
+ title: const Text("shared_album_section_people_action_remove_user")
+ .tr(),
onTap: () => removeUserFromAlbum(user),
),
];
@@ -122,18 +124,16 @@ class AlbumOptionsPage extends HookConsumerWidget {
title: Text(
album.owner.value?.name ?? "",
style: const TextStyle(
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
subtitle: Text(
album.owner.value?.email ?? "",
- style: TextStyle(color: Colors.grey[500]),
+ style: TextStyle(color: Colors.grey[600]),
),
- trailing: const Text(
+ trailing: Text(
"shared_album_section_people_owner_label",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.labelLarge,
).tr(),
);
}
@@ -152,12 +152,12 @@ class AlbumOptionsPage extends HookConsumerWidget {
title: Text(
user.name,
style: const TextStyle(
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
subtitle: Text(
user.email,
- style: TextStyle(color: Colors.grey[500]),
+ style: TextStyle(color: Colors.grey[600]),
),
trailing: userId == user.id || isOwner
? const Icon(Icons.more_horiz_rounded)
@@ -181,9 +181,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
- onPressed: () {
- context.autoPop(null);
- },
+ onPressed: () => context.autoPop(null),
),
centerTitle: true,
title: Text("translated_text_options".tr()),
@@ -209,11 +207,15 @@ class AlbumOptionsPage extends HookConsumerWidget {
dense: true,
title: Text(
"shared_album_activity_setting_title",
- style: context.textTheme.labelLarge
- ?.copyWith(fontWeight: FontWeight.bold),
+ style: context.textTheme.titleMedium
+ ?.copyWith(fontWeight: FontWeight.w500),
+ ).tr(),
+ subtitle: Text(
+ "shared_album_activity_setting_subtitle",
+ style: context.textTheme.labelLarge?.copyWith(
+ color: context.textTheme.labelLarge?.color?.withAlpha(175),
+ ),
).tr(),
- subtitle:
- const Text("shared_album_activity_setting_subtitle").tr(),
),
buildSectionTitle("shared_album_section_people_title".tr()),
buildOwnerInfo(),
diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart
index bcb32b835..6d07c3b66 100644
--- a/mobile/lib/modules/album/views/album_viewer_page.dart
+++ b/mobile/lib/modules/album/views/album_viewer_page.dart
@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
@@ -17,7 +18,6 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget {
final userId = ref.watch(authenticationProvider).userId;
final selection = useState>({});
final multiSelectEnabled = useState(false);
+ final isProcessing = useProcessingOverlay();
useEffect(
() {
@@ -75,24 +76,21 @@ class AlbumViewerPage extends HookConsumerWidget {
),
);
- if (returnPayload != null) {
+ if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
// Check if there is new assets add
- if (returnPayload.selectedAssets.isNotEmpty) {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
- var addAssetsResult =
- await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
- returnPayload.selectedAssets,
- albumInfo,
- );
+ var addAssetsResult =
+ await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
+ returnPayload.selectedAssets,
+ albumInfo,
+ );
- if (addAssetsResult != null &&
- addAssetsResult.successfullyAdded > 0) {
- ref.invalidate(albumDetailProvider(albumId));
- }
-
- ImmichLoadingOverlayController.appLoader.hide();
+ if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) {
+ ref.invalidate(albumDetailProvider(albumId));
}
+
+ isProcessing.value = false;
}
}
@@ -102,7 +100,7 @@ class AlbumViewerPage extends HookConsumerWidget {
);
if (sharedUserIds != null) {
- ImmichLoadingOverlayController.appLoader.show();
+ isProcessing.value = true;
var isSuccess = await ref
.watch(albumServiceProvider)
@@ -112,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget {
ref.invalidate(albumDetailProvider(album.id));
}
- ImmichLoadingOverlayController.appLoader.hide();
+ isProcessing.value = false;
}
}
@@ -153,10 +151,7 @@ class AlbumViewerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 8.0),
child: Text(
album.name,
- style: const TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.headlineMedium,
),
),
);
@@ -191,10 +186,7 @@ class AlbumViewerPage extends HookConsumerWidget {
),
child: Text(
dateRangeText,
- style: const TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.labelLarge,
),
);
}
@@ -266,13 +258,11 @@ class AlbumViewerPage extends HookConsumerWidget {
error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(),
),
- body: album.when(
- data: (data) => WillPopScope(
+ body: album.widgetWhen(
+ onData: (data) => WillPopScope(
onWillPop: onWillPop,
child: GestureDetector(
- onTap: () {
- titleFocusNode.unfocus();
- },
+ onTap: () => titleFocusNode.unfocus(),
child: ImmichAssetGrid(
renderList: data.renderList,
listener: selectionListener,
@@ -291,10 +281,6 @@ class AlbumViewerPage extends HookConsumerWidget {
),
),
),
- error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart
index c1870fe44..471a74ace 100644
--- a/mobile/lib/modules/album/views/asset_selection_page.dart
+++ b/mobile/lib/modules/album/views/asset_selection_page.dart
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
@@ -85,12 +86,8 @@ class AssetSelectionPage extends HookConsumerWidget {
),
],
),
- body: renderList.when(
- data: (data) => buildBody(data),
- error: (error, stackTrace) => Center(
- child: Text(error.toString()),
- ),
- loading: () => const Center(child: CircularProgressIndicator()),
+ body: renderList.widgetWhen(
+ onData: (data) => buildBody(data),
),
);
}
diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart
index 8f2c4e128..7e5fb8168 100644
--- a/mobile/lib/modules/album/views/create_album_page.dart
+++ b/mobile/lib/modules/album/views/create_album_page.dart
@@ -94,10 +94,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
- style: context.textTheme.displayMedium?.copyWith(
- fontSize: 12,
- fontWeight: FontWeight.normal,
- ),
+ style: context.textTheme.labelLarge,
).tr(),
),
);
@@ -119,7 +116,7 @@ class CreateAlbumPage extends HookConsumerWidget {
side: BorderSide(
color: context.isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
- : const Color.fromARGB(255, 206, 206, 206),
+ : const Color.fromARGB(255, 129, 129, 129),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
@@ -134,9 +131,8 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'create_shared_album_page_share_select_photos',
- style: context.textTheme.labelLarge?.copyWith(
- fontSize: 16,
- fontWeight: FontWeight.bold,
+ style: context.textTheme.titleMedium?.copyWith(
+ color: context.primaryColor,
),
).tr(),
),
@@ -222,11 +218,8 @@ class CreateAlbumPage extends HookConsumerWidget {
},
icon: const Icon(Icons.close_rounded),
),
- title: Text(
+ title: const Text(
'share_create_album',
- style: context.textTheme.displayMedium?.copyWith(
- color: context.primaryColor,
- ),
).tr(),
actions: [
if (isSharedAlbum)
diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart
index 15229232b..ce4d5e0cc 100644
--- a/mobile/lib/modules/album/views/library_page.dart
+++ b/mobile/lib/modules/album/views/library_page.dart
@@ -125,10 +125,8 @@ class LibraryPage extends HookConsumerWidget {
),
Text(
options[selectedAlbumSortOrder.value],
- style: TextStyle(
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
- fontSize: 12.0,
),
),
],
@@ -172,11 +170,9 @@ class LibraryPage extends HookConsumerWidget {
top: 8.0,
bottom: 16,
),
- child: const Text(
+ child: Text(
'library_page_new_album',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.labelLarge,
).tr(),
),
],
@@ -198,9 +194,9 @@ class LibraryPage extends HookConsumerWidget {
child: Text(
label,
style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 13.0,
- color: isDarkTheme ? Colors.white : Colors.grey[800],
+ color: context.isDarkTheme
+ ? Colors.white
+ : Colors.black.withAlpha(200),
),
),
),
@@ -278,9 +274,11 @@ class LibraryPage extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- const Text(
+ Text(
'library_page_albums',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
buildSortButton(),
],
@@ -326,9 +324,11 @@ class LibraryPage extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- const Text(
+ Text(
'library_page_device_albums',
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
],
),
diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
index b91197d28..2aad67ef5 100644
--- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
+++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
@@ -137,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
),
],
),
- body: suggestedShareUsers.when(
- data: (users) {
+ body: suggestedShareUsers.widgetWhen(
+ onData: (users) {
for (var sharedUsers in album.sharedUsers) {
users.removeWhere(
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
@@ -147,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
return buildUserList(users);
},
- error: (e, _) => Text("Error loading suggested users $e"),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
index 61ced47e2..3d6dcf678 100644
--- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
+++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
class SelectUserForSharingPage extends HookConsumerWidget {
@@ -42,7 +42,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
ScaffoldMessenger(
child: SnackBar(
- content: const Text('select_user_for_sharing_page_err_album').tr(),
+ content: Text(
+ 'select_user_for_sharing_page_err_album',
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
+ ).tr(),
),
);
}
@@ -166,14 +171,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
),
],
),
- body: suggestedShareUsers.when(
- data: (users) {
+ body: suggestedShareUsers.widgetWhen(
+ onData: (users) {
return buildUserList(users);
},
- error: (e, _) => Text("Error loading suggested users $e"),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart
index 3061289a9..2e2e44aca 100644
--- a/mobile/lib/modules/album/views/sharing_page.dart
+++ b/mobile/lib/modules/album/views/sharing_page.dart
@@ -80,25 +80,20 @@ class SharingPage extends HookConsumerWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
- fontWeight: FontWeight.bold,
- color:
- context.isDarkTheme ? context.primaryColor : Colors.black,
+ color: context.primaryColor,
+ fontWeight: FontWeight.w500,
),
),
subtitle: isOwner
? Text(
'album_thumbnail_owned'.tr(),
- style: const TextStyle(
- fontSize: 12.0,
- ),
+ style: context.textTheme.bodyMedium,
)
: album.ownerName != null
? Text(
'album_thumbnail_shared_by'
.tr(args: [album.ownerName!]),
- style: const TextStyle(
- fontSize: 12.0,
- ),
+ style: context.textTheme.bodyMedium,
)
: null,
onTap: () {
@@ -137,8 +132,8 @@ class SharingPage extends HookConsumerWidget {
"sharing_silver_appbar_create_shared_album",
maxLines: 1,
style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 11,
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
),
).tr(),
),
@@ -154,8 +149,8 @@ class SharingPage extends HookConsumerWidget {
label: const Text(
"sharing_silver_appbar_shared_links",
style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 11,
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
),
maxLines: 1,
).tr(),
@@ -236,9 +231,11 @@ class SharingPage extends HookConsumerWidget {
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverToBoxAdapter(
- child: const Text(
+ child: Text(
"partner_page_title",
- style: TextStyle(fontWeight: FontWeight.bold),
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
),
),
@@ -246,10 +243,10 @@ class SharingPage extends HookConsumerWidget {
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverToBoxAdapter(
- child: const Text(
+ child: Text(
"sharing_page_album",
- style: TextStyle(
- fontWeight: FontWeight.bold,
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
),
).tr(),
),
diff --git a/mobile/lib/modules/archive/views/archive_page.dart b/mobile/lib/modules/archive/views/archive_page.dart
index 06270afc1..fb3cecc10 100644
--- a/mobile/lib/modules/archive/views/archive_page.dart
+++ b/mobile/lib/modules/archive/views/archive_page.dart
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget {
child: SizedBox(
height: 64,
child: Card(
- child: Column(
- children: [
- ListTile(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10),
- ),
- leading: const Icon(
- Icons.unarchive_rounded,
- ),
- title: Text(
- 'control_bottom_app_bar_unarchive'.tr(),
- style: const TextStyle(fontSize: 14),
- ),
- onTap: processing.value
- ? null
- : () async {
- processing.value = true;
- try {
- await handleArchiveAssets(
- ref,
- context,
- selection.value.toList(),
- shouldArchive: false,
- );
- } finally {
- processing.value = false;
- selectionEnabledHook.value = false;
- }
- },
- ),
- ],
+ child: ListTile(
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(10)),
+ ),
+ leading: const Icon(
+ Icons.unarchive_rounded,
+ ),
+ title: Text(
+ 'control_bottom_app_bar_unarchive'.tr(),
+ style: const TextStyle(fontSize: 14),
+ ),
+ onTap: processing.value
+ ? null
+ : () async {
+ processing.value = true;
+ try {
+ await handleArchiveAssets(
+ ref,
+ context,
+ selection.value.toList(),
+ shouldArchive: false,
+ );
+ } finally {
+ processing.value = false;
+ selectionEnabledHook.value = false;
+ }
+ },
),
),
),
@@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget {
);
}
- return archivedAssets.when(
- loading: () => Scaffold(
- appBar: buildAppBar("?"),
- body: const Center(child: CircularProgressIndicator()),
+ return Scaffold(
+ appBar: archivedAssets.maybeWhen(
+ data: (data) => buildAppBar(data.totalAssets.toString()),
+ orElse: () => buildAppBar("?"),
),
- error: (error, stackTrace) => Scaffold(
- appBar: buildAppBar("Error"),
- body: Center(child: Text(error.toString())),
- ),
- data: (data) => Scaffold(
- appBar: buildAppBar(data.totalAssets.toString()),
- body: data.isEmpty
+ body: archivedAssets.widgetWhen(
+ onData: (data) => data.isEmpty
? Center(
child: Text('archive_page_no_archived_assets'.tr()),
)
diff --git a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
index 3a356c840..27be7029d 100644
--- a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
+++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
@@ -1,6 +1,5 @@
import 'dart:io';
-import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
@@ -84,8 +83,8 @@ class ImageViewerService {
}
return entity != null;
}
- } catch (e) {
- debugPrint("Error saving file $e");
+ } catch (error, stack) {
+ _log.severe("Error saving file ${error.toString()}", error, stack);
return false;
}
}
diff --git a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
index 97b955b5f..c265346b0 100644
--- a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
+++ b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
@@ -62,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget {
ClipboardData(text: assetDetail.toString()),
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text("Copied to clipboard"),
+ SnackBar(
+ content: Text(
+ "Copied to clipboard",
+ style: context.textTheme.bodyLarge
+ ?.copyWith(
+ color: context.primaryColor,
+ ),
+ ),
),
);
});
diff --git a/mobile/lib/modules/asset_viewer/ui/description_input.dart b/mobile/lib/modules/asset_viewer/ui/description_input.dart
index a8cb62524..c5972a822 100644
--- a/mobile/lib/modules/asset_viewer/ui/description_input.dart
+++ b/mobile/lib/modules/asset_viewer/ui/description_input.dart
@@ -93,15 +93,11 @@ class DescriptionInput extends HookConsumerWidget {
maxLines: null,
keyboardType: TextInputType.multiline,
controller: controller,
- style: const TextStyle(
- fontSize: 14,
- ),
+ style: context.textTheme.labelLarge,
decoration: InputDecoration(
hintText: 'description_input_hint_text'.tr(),
border: InputBorder.none,
- hintStyle: TextStyle(
- fontWeight: FontWeight.normal,
- fontSize: 12,
+ hintStyle: context.textTheme.labelLarge?.copyWith(
color: textColor.withOpacity(0.5),
),
suffixIcon: suffixIcon,
diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
index 4f2fbf50d..08a6a0515 100644
--- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
+++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
@@ -193,21 +193,15 @@ class ExifBottomSheet extends HookConsumerWidget {
children: [
Text(
"exif_bottom_sheet_location",
- style: TextStyle(
- fontSize: 11,
- color: textColor,
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelMedium?.copyWith(
+ color: context.textTheme.labelMedium?.color?.withAlpha(200),
+ fontWeight: FontWeight.w600,
),
).tr(),
buildMap(),
RichText(
text: TextSpan(
- style: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.bold,
- color: textColor,
- fontFamily: 'WorkSans',
- ),
+ style: context.textTheme.labelLarge,
children: [
if (exifInfo != null && exifInfo.city != null)
TextSpan(
@@ -228,7 +222,9 @@ class ExifBottomSheet extends HookConsumerWidget {
),
Text(
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
- style: const TextStyle(fontSize: 12),
+ style: context.textTheme.labelMedium?.copyWith(
+ color: context.textTheme.labelMedium?.color?.withAlpha(150),
+ ),
),
],
),
@@ -258,10 +254,7 @@ class ExifBottomSheet extends HookConsumerWidget {
titleAlignment: ListTileTitleAlignment.center,
title: Text(
title,
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: textColor,
- ),
+ style: context.textTheme.labelLarge,
),
subtitle: subtitle,
);
@@ -278,7 +271,7 @@ class ExifBottomSheet extends HookConsumerWidget {
// There is both filename and size information
return createImagePropertiesListStyle(
asset.fileName,
- Text(imgSizeString),
+ Text(imgSizeString, style: context.textTheme.bodySmall),
);
} else if (imgSizeString != null && asset.fileName.isEmpty) {
// There is only size information
@@ -305,10 +298,9 @@ class ExifBottomSheet extends HookConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
- style: TextStyle(
- fontSize: 11,
- color: textColor,
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelMedium?.copyWith(
+ color: context.textTheme.labelMedium?.color?.withAlpha(200),
+ fontWeight: FontWeight.w600,
),
).tr(),
),
@@ -323,10 +315,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
title: Text(
"${exifInfo!.make} ${exifInfo.model}",
- style: TextStyle(
- color: textColor,
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.labelLarge,
),
subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
@@ -334,6 +323,7 @@ class ExifBottomSheet extends HookConsumerWidget {
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
+ style: context.textTheme.bodySmall,
)
: null,
),
diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
index bdb2bb50c..9948ab4cc 100644
--- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
+++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
@@ -514,7 +514,7 @@ class GalleryViewerPage extends HookConsumerWidget {
currentAsset,
stackElements.elementAt(stackIndex.value),
);
- Navigator.pop(ctx);
+ ctx.pop();
context.autoPop();
},
title: const Text(
@@ -541,7 +541,7 @@ class GalleryViewerPage extends HookConsumerWidget {
stackElements.elementAt(1),
childrenToRemove: [currentAsset],
);
- Navigator.pop(ctx);
+ ctx.pop();
context.autoPop();
} else {
await ref.read(assetStackServiceProvider).updateStack(
@@ -551,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget {
],
);
removeAssetFromStack();
- Navigator.pop(ctx);
+ ctx.pop();
}
},
title: const Text(
@@ -569,7 +569,7 @@ class GalleryViewerPage extends HookConsumerWidget {
currentAsset,
childrenToRemove: stack,
);
- Navigator.pop(ctx);
+ ctx.pop();
context.autoPop();
},
title: const Text(
diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart
index 15cc0c349..f4ca5932a 100644
--- a/mobile/lib/modules/backup/services/backup.service.dart
+++ b/mobile/lib/modules/backup/services/backup.service.dart
@@ -42,6 +42,9 @@ class BackupService {
try {
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
+
+ // TODO! Start using this in 1.92.0
+ // return await _apiService.assetApi.getAllUserAssetsByDeviceId(deviceId);
} catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
return null;
@@ -275,13 +278,6 @@ class BackupService {
req.files.add(assetRawUploadData);
- if (entity.isLivePhoto) {
- var livePhotoRawUploadData = await _getLivePhotoFile(entity);
- if (livePhotoRawUploadData != null) {
- req.files.add(livePhotoRawUploadData);
- }
- }
-
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
@@ -296,6 +292,29 @@ class BackupService {
var response =
await httpClient.send(req, cancellationToken: cancelToken);
+ // Send live photo separately
+ if (entity.isLivePhoto) {
+ var livePhotoRawUploadData = await _getLivePhotoFile(entity);
+ if (livePhotoRawUploadData != null) {
+ var livePhotoReq = MultipartRequest(
+ req.method,
+ req.url,
+ onProgress: req.onProgress,
+ )
+ ..headers.addAll(req.headers)
+ ..fields.addAll(req.fields);
+
+ livePhotoReq.files.add(livePhotoRawUploadData);
+ // Send live photo only if the non-motion part is successful
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ response = await httpClient.send(
+ livePhotoReq,
+ cancellationToken: cancelToken,
+ );
+ }
+ }
+ }
+
if (response.statusCode == 200) {
// asset is a duplicate (already exists on the server)
duplicatedAssetIds.add(entity.id);
@@ -353,7 +372,7 @@ class BackupService {
var fileStream = motionFile.openRead();
String fileName = p.basename(motionFile.path);
return http.MultipartFile(
- "livePhotoData",
+ "assetData",
fileStream,
motionFile.lengthSync(),
filename: fileName,
diff --git a/mobile/lib/modules/backup/ui/backup_info_card.dart b/mobile/lib/modules/backup/ui/backup_info_card.dart
index a398bc83e..4feccd19b 100644
--- a/mobile/lib/modules/backup/ui/backup_info_card.dart
+++ b/mobile/lib/modules/backup/ui/backup_info_card.dart
@@ -28,17 +28,17 @@ class BackupInfoCard extends StatelessWidget {
elevation: 0,
borderOnForeground: false,
child: ListTile(
- minVerticalPadding: 15,
+ minVerticalPadding: 18,
isThreeLine: true,
title: Text(
title,
- style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
+ style: context.textTheme.titleMedium,
),
subtitle: Padding(
- padding: const EdgeInsets.only(top: 8.0),
+ padding: const EdgeInsets.only(top: 4.0, right: 18.0),
child: Text(
subtitle,
- style: const TextStyle(fontSize: 12),
+ style: context.textTheme.bodyMedium,
),
),
trailing: Column(
@@ -46,9 +46,12 @@ class BackupInfoCard extends StatelessWidget {
children: [
Text(
info,
- style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ style: context.textTheme.titleLarge,
),
- const Text("backup_info_card_assets").tr(),
+ Text(
+ "backup_info_card_assets",
+ style: context.textTheme.labelLarge,
+ ).tr(),
],
),
),
diff --git a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
index 0ddc6beaf..926ccd25b 100644
--- a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
+++ b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
@@ -188,9 +188,9 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- const Text(
+ Text(
"backup_controller_page_uploading_file_info",
- style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+ style: context.textTheme.titleSmall,
).tr(),
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
],
diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart
index 7e0a0b513..96ed879ac 100644
--- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart
+++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart
@@ -100,7 +100,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
label: Text(
album.name,
style: TextStyle(
- fontSize: 10,
+ fontSize: 12,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
@@ -134,7 +134,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
label: Text(
album.name,
style: TextStyle(
- fontSize: 10,
+ fontSize: 12,
color: isDarkTheme ? Colors.black : immichBackgroundColor,
fontWeight: FontWeight.bold,
),
@@ -203,7 +203,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
),
title: const Text(
"backup_album_selection_page_select_albums",
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
).tr(),
elevation: 0,
),
@@ -219,12 +218,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
vertical: 8.0,
horizontal: 16.0,
),
- child: const Text(
+ child: Text(
"backup_album_selection_page_selection_info",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
+ style: context.textTheme.titleSmall,
).tr(),
),
// Selected Album Chips
@@ -250,19 +246,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
.toString(),
],
),
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
+ style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
- style: TextStyle(
- fontSize: 12,
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
- fontWeight: FontWeight.bold,
),
).tr(),
),
diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart
index 6a949213a..2bdb3a5dd 100644
--- a/mobile/lib/modules/backup/views/backup_controller_page.dart
+++ b/mobile/lib/modules/backup/views/backup_controller_page.dart
@@ -193,7 +193,7 @@ class BackupControllerPage extends HookConsumerWidget {
: const Icon(Icons.cloud_off_rounded),
title: Text(
backUpOption,
- style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+ style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -213,9 +213,8 @@ class BackupControllerPage extends HookConsumerWidget {
.setAutoBackup(!isAutoBackup),
child: Text(
backupBtnText,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 12,
+ style: context.textTheme.labelLarge?.copyWith(
+ color: context.isDarkTheme ? Colors.black : Colors.white,
),
),
),
@@ -230,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget {
final snackBar = SnackBar(
content: Text(
msg.tr(),
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
),
backgroundColor: Colors.red,
);
@@ -335,7 +337,7 @@ class BackupControllerPage extends HookConsumerWidget {
isBackgroundEnabled
? "backup_controller_page_background_is_on"
: "backup_controller_page_background_is_off",
- style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+ style: context.textTheme.titleSmall,
).tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -426,9 +428,8 @@ class BackupControllerPage extends HookConsumerWidget {
isBackgroundEnabled
? "backup_controller_page_background_turn_off"
: "backup_controller_page_background_turn_on",
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 12,
+ style: context.textTheme.labelLarge?.copyWith(
+ color: context.isDarkTheme ? Colors.black : Colors.white,
),
).tr(),
),
@@ -511,10 +512,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
text.trim().substring(0, text.length - 2),
- style: TextStyle(
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
- fontSize: 12,
- fontWeight: FontWeight.bold,
),
),
);
@@ -523,10 +522,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"backup_controller_page_none_selected".tr(),
- style: TextStyle(
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
- fontSize: 12,
- fontWeight: FontWeight.bold,
),
),
);
@@ -546,10 +543,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
text.trim().substring(0, text.length - 2),
- style: TextStyle(
+ style: context.textTheme.labelLarge?.copyWith(
color: Colors.red[300],
- fontSize: 12,
- fontWeight: FontWeight.bold,
),
),
);
@@ -559,55 +554,57 @@ class BackupControllerPage extends HookConsumerWidget {
}
buildFolderSelectionTile() {
- return Card(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- side: BorderSide(
- color: context.isDarkTheme
- ? const Color.fromARGB(255, 56, 56, 56)
- : Colors.black12,
- width: 1,
- ),
- ),
- elevation: 0,
- borderOnForeground: false,
- child: ListTile(
- minVerticalPadding: 15,
- title: const Text(
- "backup_controller_page_albums",
- style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
- ).tr(),
- subtitle: Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- "backup_controller_page_to_backup",
- style: TextStyle(fontSize: 12),
- ).tr(),
- buildSelectedAlbumName(),
- buildExcludedAlbumName(),
- ],
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ side: BorderSide(
+ color: context.isDarkTheme
+ ? const Color.fromARGB(255, 56, 56, 56)
+ : Colors.black12,
+ width: 1,
),
),
- trailing: ElevatedButton(
- onPressed: () async {
- await context.autoPush(const BackupAlbumSelectionRoute());
- // waited until returning from selection
- await ref
- .read(backupProvider.notifier)
- .backupAlbumSelectionDone();
- // waited until backup albums are stored in DB
- ref.read(albumProvider.notifier).getDeviceAlbums();
- },
- child: const Text(
- "backup_controller_page_select",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 12,
- ),
+ elevation: 0,
+ borderOnForeground: false,
+ child: ListTile(
+ minVerticalPadding: 18,
+ title: Text(
+ "backup_controller_page_albums",
+ style: context.textTheme.titleMedium,
).tr(),
+ subtitle: Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "backup_controller_page_to_backup",
+ style: context.textTheme.bodyMedium,
+ ).tr(),
+ buildSelectedAlbumName(),
+ buildExcludedAlbumName(),
+ ],
+ ),
+ ),
+ trailing: ElevatedButton(
+ onPressed: () async {
+ await context.autoPush(const BackupAlbumSelectionRoute());
+ // waited until returning from selection
+ await ref
+ .read(backupProvider.notifier)
+ .backupAlbumSelectionDone();
+ // waited until backup albums are stored in DB
+ ref.read(albumProvider.notifier).getDeviceAlbums();
+ },
+ child: const Text(
+ "backup_controller_page_select",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ).tr(),
+ ),
),
),
);
@@ -657,7 +654,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: const Text(
"backup_controller_page_start_backup",
style: TextStyle(
- fontSize: 14,
+ fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
@@ -680,7 +677,6 @@ class BackupControllerPage extends HookConsumerWidget {
elevation: 0,
title: const Text(
"backup_controller_page_backup",
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
).tr(),
leading: IconButton(
onPressed: () {
@@ -735,7 +731,6 @@ class BackupControllerPage extends HookConsumerWidget {
if (showBackupFix) const Divider(),
if (showBackupFix) buildCheckCorruptBackups(),
const Divider(),
- const Divider(),
const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(),
diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart
index 163611396..095297507 100644
--- a/mobile/lib/modules/favorite/views/favorites_page.dart
+++ b/mobile/lib/modules/favorite/views/favorites_page.dart
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget {
child: SizedBox(
height: 64,
child: Card(
- child: Column(
- children: [
- ListTile(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10),
- ),
- leading: const Icon(
- Icons.star_border,
- ),
- title: const Text(
- "Unfavorite",
- style: TextStyle(fontSize: 14),
- ),
- onTap: processing.value ? null : unfavorite,
- ),
- ],
+ child: ListTile(
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(10)),
+ ),
+ leading: const Icon(
+ Icons.star_border,
+ ),
+ title: const Text(
+ "Unfavorite",
+ style: TextStyle(fontSize: 14),
+ ),
+ onTap: processing.value ? null : unfavorite,
),
),
),
@@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget {
return Scaffold(
appBar: buildAppBar(),
- body: ref.watch(favoriteAssetsProvider).when(
- loading: () => const Center(child: CircularProgressIndicator()),
- error: (error, stackTrace) => Center(child: Text(error.toString())),
- data: (data) => data.isEmpty
+ body: ref.watch(favoriteAssetsProvider).widgetWhen(
+ onData: (data) => data.isEmpty
? Center(
child: Text('favorites_page_no_favorites'.tr()),
)
diff --git a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
index acb176aaa..1455f53ef 100644
--- a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
+import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
-class GroupDividerTitle extends ConsumerWidget {
+class GroupDividerTitle extends HookConsumerWidget {
const GroupDividerTitle({
Key? key,
required this.text,
@@ -21,6 +25,18 @@ class GroupDividerTitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final appSettingService = ref.watch(appSettingsServiceProvider);
+ final groupBy = useState(GroupAssetsBy.day);
+
+ useEffect(
+ () {
+ groupBy.value = GroupAssetsBy.values[
+ appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)];
+ return null;
+ },
+ [],
+ );
+
void handleTitleIconClick() {
HapticFeedback.heavyImpact();
if (selected) {
@@ -31,8 +47,8 @@ class GroupDividerTitle extends ConsumerWidget {
}
return Padding(
- padding: const EdgeInsets.only(
- top: 12.0,
+ padding: EdgeInsets.only(
+ top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0,
bottom: 16.0,
left: 12.0,
right: 12.0,
@@ -41,10 +57,14 @@ class GroupDividerTitle extends ConsumerWidget {
children: [
Text(
text,
- style: const TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- ),
+ style: groupBy.value == GroupAssetsBy.month
+ ? context.textTheme.bodyLarge?.copyWith(
+ fontSize: 24.0,
+ )
+ : context.textTheme.labelLarge?.copyWith(
+ color: context.textTheme.labelLarge?.color?.withAlpha(250),
+ fontWeight: FontWeight.w500,
+ ),
),
const Spacer(),
GestureDetector(
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
index 2c0f63394..562b7892c 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
@@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class ImmichAssetGrid extends HookConsumerWidget {
@@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget {
if (renderList != null) return buildAssetGridView(renderList!);
final renderListFuture = ref.watch(renderListProvider(assets!));
- return renderListFuture.when(
- data: (renderList) => buildAssetGridView(renderList),
- error: (err, stack) => Center(child: Text("$err")),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
+ return renderListFuture.widgetWhen(
+ onData: (renderList) => buildAssetGridView(renderList),
);
}
}
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
index 27b9f9d3d..77940d254 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
@@ -222,10 +222,9 @@ class ImmichAssetGridViewState extends State {
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
child: Text(
title,
- style: TextStyle(
+ style: const TextStyle(
fontSize: 26,
- fontWeight: FontWeight.bold,
- color: context.textTheme.displayLarge?.color,
+ fontWeight: FontWeight.w500,
),
),
);
diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
index c99e08fb3..694279c0d 100644
--- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
@@ -197,7 +197,9 @@ class ThumbnailImage extends StatelessWidget {
},
child: Stack(
children: [
- Container(
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.decelerate,
decoration: BoxDecoration(
border: multiselectEnabled && isSelected
? Border.all(
diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart
index 799f7296a..4f2438577 100644
--- a/mobile/lib/modules/home/views/home_page.dart
+++ b/mobile/lib/modules/home/views/home_page.dart
@@ -28,6 +28,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
class HomePage extends HookConsumerWidget {
@@ -50,7 +51,7 @@ class HomePage extends HookConsumerWidget {
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
- final processing = useState(false);
+ final processing = useProcessingOverlay();
useEffect(
() {
@@ -235,10 +236,10 @@ class HomePage extends HookConsumerWidget {
processing.value = true;
selectionEnabledHook.value = false;
try {
- ref.read(manualUploadProvider.notifier).uploadAssets(
- context,
- selection.value.where((a) => a.storage == AssetState.local),
- );
+ ref.read(manualUploadProvider.notifier).uploadAssets(
+ context,
+ selection.value.where((a) => a.storage == AssetState.local),
+ );
} finally {
processing.value = false;
}
@@ -346,16 +347,12 @@ class HomePage extends HookConsumerWidget {
} else {
refreshCount.value++;
// set counter back to 0 if user does not request refresh again
- Timer(const Duration(seconds: 4), () {
- refreshCount.value = 0;
- });
+ Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
}
}
buildLoadingIndicator() {
- Timer(const Duration(seconds: 2), () {
- tipOneOpacity.value = 1;
- });
+ Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
return Center(
child: Column(
@@ -439,7 +436,6 @@ class HomePage extends HookConsumerWidget {
selectionAssetState: selectionAssetState.value,
onStack: onStack,
),
- if (processing.value) const Center(child: ImmichLoadingIndicator()),
],
),
);
diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart
index 835907d05..6c12742e4 100644
--- a/mobile/lib/modules/login/ui/login_form.dart
+++ b/mobile/lib/modules/login/ui/login_form.dart
@@ -48,7 +48,7 @@ class LoginForm extends HookConsumerWidget {
/// Fetch the server login credential and enables oAuth login if necessary
/// Returns true if successful, false otherwise
Future getServerLoginCredential() async {
- final serverUrl = serverEndpointController.text.trim();
+ final serverUrl = sanitizeUrl(serverEndpointController.text);
// Guard empty URL
if (serverUrl.isEmpty) {
@@ -127,6 +127,12 @@ class LoginForm extends HookConsumerWidget {
);
populateTestLoginInfo() {
+ usernameController.text = 'demo@immich.app';
+ passwordController.text = 'demo';
+ serverEndpointController.text = 'https://demo.immich.app';
+ }
+
+ populateTestLoginInfo1() {
usernameController.text = 'testuser@email.com';
passwordController.text = 'password';
serverEndpointController.text = 'http://10.1.15.216:2283/api';
@@ -144,7 +150,7 @@ class LoginForm extends HookConsumerWidget {
await ref.read(authenticationProvider.notifier).login(
usernameController.text,
passwordController.text,
- serverEndpointController.text.trim(),
+ sanitizeUrl(serverEndpointController.text),
);
if (isAuthenticated) {
// Resume backup (if enable) then navigate
@@ -181,7 +187,7 @@ class LoginForm extends HookConsumerWidget {
try {
oAuthServerConfig = await oAuthService
- .getOAuthServerConfig(serverEndpointController.text);
+ .getOAuthServerConfig(sanitizeUrl(serverEndpointController.text));
isLoading.value = true;
} catch (e) {
@@ -203,7 +209,7 @@ class LoginForm extends HookConsumerWidget {
.watch(authenticationProvider.notifier)
.setSuccessLoginInfo(
accessToken: loginResponseDto.accessToken,
- serverUrl: serverEndpointController.text,
+ serverUrl: sanitizeUrl(serverEndpointController.text),
);
if (isSuccess) {
@@ -299,7 +305,7 @@ class LoginForm extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
- serverEndpointController.text,
+ sanitizeUrl(serverEndpointController.text),
style: context.textTheme.displaySmall,
textAlign: TextAlign.center,
),
@@ -387,6 +393,7 @@ class LoginForm extends HookConsumerWidget {
children: [
GestureDetector(
onDoubleTap: () => populateTestLoginInfo(),
+ onLongPress: () => populateTestLoginInfo1(),
child: RotationTransition(
turns: logoAnimationController,
child: const ImmichLogo(
diff --git a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart b/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
index 20d4654b6..7e8ff13d0 100644
--- a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
+++ b/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
@@ -179,7 +179,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState {
? "map_assets_in_bounds".plural(assetsInBound.value.length)
: "map_no_assets_in_bounds".tr();
final dragHandle = Container(
- height: 60,
+ height: 70,
width: double.infinity,
decoration: BoxDecoration(
color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
@@ -195,11 +195,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState {
const SizedBox(height: 15),
Text(
textToDisplay,
- style: TextStyle(
- fontSize: 16,
- color: context.textTheme.displayLarge?.color,
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.bodyLarge,
),
Divider(
height: 10,
diff --git a/mobile/lib/modules/map/ui/map_settings_dialog.dart b/mobile/lib/modules/map/ui/map_settings_dialog.dart
index 1d1e8b4c0..9619bd661 100644
--- a/mobile/lib/modules/map/ui/map_settings_dialog.dart
+++ b/mobile/lib/modules/map/ui/map_settings_dialog.dart
@@ -134,12 +134,16 @@ class MapSettingsDialog extends HookConsumerWidget {
backgroundColor:
mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700],
),
- child: Text(
- "map_settings_dialog_cancel".tr(),
- style: theme.textTheme.labelSmall?.copyWith(
- fontWeight: FontWeight.bold,
- color:
- mapSettings.isDarkTheme ? Colors.grey[900] : Colors.grey[100],
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Text(
+ "map_settings_dialog_cancel".tr(),
+ style: theme.textTheme.labelLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ color: mapSettings.isDarkTheme
+ ? Colors.grey[900]
+ : Colors.grey[100],
+ ),
),
),
),
@@ -155,11 +159,14 @@ class MapSettingsDialog extends HookConsumerWidget {
style: TextButton.styleFrom(
backgroundColor: theme.primaryColor,
),
- child: Text(
- "map_settings_dialog_save".tr(),
- style: theme.textTheme.labelSmall?.copyWith(
- fontWeight: FontWeight.bold,
- color: theme.primaryTextTheme.labelLarge?.color,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Text(
+ "map_settings_dialog_save".tr(),
+ style: theme.textTheme.labelLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ color: theme.primaryTextTheme.labelLarge?.color,
+ ),
),
),
),
diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart
index 8ef06e0d1..6c27ea748 100644
--- a/mobile/lib/modules/memories/ui/memory_card.dart
+++ b/mobile/lib/modules/memories/ui/memory_card.dart
@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:cached_network_image/cached_network_image.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/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -34,10 +35,9 @@ class MemoryCard extends HookConsumerWidget {
buildTitle() {
return Text(
title,
- style: const TextStyle(
+ style: context.textTheme.headlineMedium?.copyWith(
color: Colors.white,
- fontWeight: FontWeight.bold,
- fontSize: 24.0,
+ fontWeight: FontWeight.w500,
),
);
}
diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart
index 65f0c6525..0c709919b 100644
--- a/mobile/lib/modules/memories/ui/memory_lane.dart
+++ b/mobile/lib/modules/memories/ui/memory_lane.dart
@@ -17,7 +17,7 @@ class MemoryLane extends HookConsumerWidget {
.whenData(
(memories) => memories != null
? Container(
- margin: const EdgeInsets.only(top: 10),
+ margin: const EdgeInsets.only(top: 10, left: 10),
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal,
@@ -48,7 +48,7 @@ class MemoryLane extends HookConsumerWidget {
clipBehavior: Clip.hardEdge,
child: ColorFiltered(
colorFilter: ColorFilter.mode(
- Colors.black.withOpacity(0.1),
+ Colors.black.withOpacity(0.2),
BlendMode.darken,
),
child: ImmichImage(
@@ -71,9 +71,9 @@ class MemoryLane extends HookConsumerWidget {
child: Text(
memory.title,
style: const TextStyle(
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w600,
color: Colors.white,
- fontSize: 14,
+ fontSize: 15,
),
),
),
diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart
index ca88ed04d..9c135961e 100644
--- a/mobile/lib/modules/memories/views/memory_page.dart
+++ b/mobile/lib/modules/memories/views/memory_page.dart
@@ -138,8 +138,8 @@ class MemoryPage extends HookConsumerWidget {
memory.title,
style: TextStyle(
color: Colors.grey[400],
- fontSize: 11.0,
- fontWeight: FontWeight.w600,
+ fontSize: 13.0,
+ fontWeight: FontWeight.w500,
),
),
Text(
@@ -148,7 +148,7 @@ class MemoryPage extends HookConsumerWidget {
),
style: const TextStyle(
color: Colors.white,
- fontSize: 14.0,
+ fontSize: 15.0,
fontWeight: FontWeight.w500,
),
),
diff --git a/mobile/lib/modules/partner/ui/partner_list.dart b/mobile/lib/modules/partner/ui/partner_list.dart
index ffd2bdf86..6cf330509 100644
--- a/mobile/lib/modules/partner/ui/partner_list.dart
+++ b/mobile/lib/modules/partner/ui/partner_list.dart
@@ -28,15 +28,11 @@ class PartnerList extends HookConsumerWidget {
leading: userAvatar(context, p, radius: 24),
title: Text(
"${p.name}'s photos",
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
+ style: context.textTheme.labelLarge,
),
trailing: Text(
"View all",
- style: TextStyle(
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
),
diff --git a/mobile/lib/modules/partner/views/partner_detail_page.dart b/mobile/lib/modules/partner/views/partner_detail_page.dart
index f7b1580b2..28d53646d 100644
--- a/mobile/lib/modules/partner/views/partner_detail_page.dart
+++ b/mobile/lib/modules/partner/views/partner_detail_page.dart
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class PartnerDetailPage extends HookConsumerWidget {
@@ -71,8 +71,8 @@ class PartnerDetailPage extends HookConsumerWidget {
),
],
),
- body: assets.when(
- data: (renderList) => renderList.isEmpty
+ body: assets.widgetWhen(
+ onData: (renderList) => renderList.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Text(
@@ -84,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget {
onRefresh: () =>
ref.read(assetProvider.notifier).getPartnerAssets(partner),
),
- error: (e, _) => Text("Error loading partners:\n$e"),
- loading: () => const Center(child: ImmichLoadingIndicator()),
),
);
}
diff --git a/mobile/lib/modules/partner/views/partner_page.dart b/mobile/lib/modules/partner/views/partner_page.dart
index bb567b62b..e90250df5 100644
--- a/mobile/lib/modules/partner/views/partner_page.dart
+++ b/mobile/lib/modules/partner/views/partner_page.dart
@@ -1,6 +1,7 @@
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/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -34,7 +35,7 @@ class PartnerPage extends HookConsumerWidget {
children: [
for (User u in users)
SimpleDialogOption(
- onPressed: () => Navigator.pop(context, u),
+ onPressed: () => context.pop(u),
child: Row(
children: [
Padding(
@@ -70,8 +71,7 @@ class PartnerPage extends HookConsumerWidget {
builder: (BuildContext context) {
return ConfirmDialog(
title: "partner_page_stop_sharing_title",
- content:
- "partner_page_stop_sharing_content".tr(args: [u.name]),
+ content: "partner_page_stop_sharing_content".tr(args: [u.name]),
onOk: () => ref.read(partnerServiceProvider).removePartner(u),
);
},
@@ -118,6 +118,7 @@ class PartnerPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -126,12 +127,15 @@ class PartnerPage extends HookConsumerWidget {
style: TextStyle(fontSize: 14),
).tr(),
),
- ElevatedButton.icon(
- onPressed: availableUsers.whenOrNull(
- data: (data) => addNewUsersHandler,
+ Align(
+ alignment: Alignment.center,
+ child: ElevatedButton.icon(
+ onPressed: availableUsers.whenOrNull(
+ data: (data) => addNewUsersHandler,
+ ),
+ icon: const Icon(Icons.person_add),
+ label: const Text("partner_page_add_partner").tr(),
),
- icon: const Icon(Icons.person_add),
- label: const Text("partner_page_add_partner").tr(),
),
],
),
diff --git a/mobile/lib/modules/search/providers/people.provider.dart b/mobile/lib/modules/search/providers/people.provider.dart
index e40ff3fc8..6009ee53a 100644
--- a/mobile/lib/modules/search/providers/people.provider.dart
+++ b/mobile/lib/modules/search/providers/people.provider.dart
@@ -1,44 +1,51 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/services/person.service.dart';
-import 'package:openapi/api.dart';
+import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
+import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
-final personAssetsProvider = FutureProvider.family
- .autoDispose((ref, personId) async {
- final PersonService personService = ref.watch(personServiceProvider);
+part 'people.provider.g.dart';
+@riverpod
+Future> getCuratedPeople(
+ GetCuratedPeopleRef ref,
+) async {
+ final PersonService personService = ref.read(personServiceProvider);
+
+ final curatedPeople = await personService.getCuratedPeople();
+
+ return curatedPeople
+ .map((p) => CuratedContent(id: p.id, label: p.name))
+ .toList();
+}
+
+@riverpod
+Future personAssets(PersonAssetsRef ref, String personId) async {
+ final PersonService personService = ref.read(personServiceProvider);
final assets = await personService.getPersonAssets(personId);
-
if (assets == null) {
return RenderList.empty();
}
- return RenderList.fromAssets(assets, GroupAssetsBy.auto);
-});
-
-final getCuratedPeopleProvider =
- FutureProvider.autoDispose>((ref) async {
- final PersonService personService = ref.watch(personServiceProvider);
-
- final curatedPeople = await personService.getCuratedPeople();
-
- return curatedPeople ?? [];
-});
-
-class UpdatePersonName {
- final String id;
- final String name;
-
- UpdatePersonName(this.id, this.name);
+ final settings = ref.read(appSettingsServiceProvider);
+ final groupBy =
+ GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
+ return await RenderList.fromAssets(assets, groupBy);
}
-final updatePersonNameProvider =
- StateProvider.family((ref, dto) async {
- final PersonService personService = ref.watch(personServiceProvider);
+@riverpod
+Future updatePersonName(
+ UpdatePersonNameRef ref,
+ String personId,
+ String updatedName,
+) async {
+ final PersonService personService = ref.read(personServiceProvider);
+ final person = await personService.updateName(personId, updatedName);
- final person = await personService.updateName(dto.id, dto.name);
-
- if (person != null && person.name == dto.name) {
+ if (person != null && person.name == updatedName) {
ref.invalidate(getCuratedPeopleProvider);
+ return true;
}
-});
+ return false;
+}
diff --git a/mobile/lib/modules/search/providers/people.provider.g.dart b/mobile/lib/modules/search/providers/people.provider.g.dart
new file mode 100644
index 000000000..c13c2c160
--- /dev/null
+++ b/mobile/lib/modules/search/providers/people.provider.g.dart
@@ -0,0 +1,320 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'people.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0';
+
+/// See also [getCuratedPeople].
+@ProviderFor(getCuratedPeople)
+final getCuratedPeopleProvider =
+ AutoDisposeFutureProvider>.internal(
+ getCuratedPeople,
+ name: r'getCuratedPeopleProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$getCuratedPeopleHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef GetCuratedPeopleRef
+ = AutoDisposeFutureProviderRef>;
+String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
+
+/// Copied from Dart SDK
+class _SystemHash {
+ _SystemHash._();
+
+ static int combine(int hash, int value) {
+ // ignore: parameter_assignments
+ hash = 0x1fffffff & (hash + value);
+ // ignore: parameter_assignments
+ hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+ return hash ^ (hash >> 6);
+ }
+
+ static int finish(int hash) {
+ // ignore: parameter_assignments
+ hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+ // ignore: parameter_assignments
+ hash = hash ^ (hash >> 11);
+ return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+ }
+}
+
+/// See also [personAssets].
+@ProviderFor(personAssets)
+const personAssetsProvider = PersonAssetsFamily();
+
+/// See also [personAssets].
+class PersonAssetsFamily extends Family> {
+ /// See also [personAssets].
+ const PersonAssetsFamily();
+
+ /// See also [personAssets].
+ PersonAssetsProvider call(
+ String personId,
+ ) {
+ return PersonAssetsProvider(
+ personId,
+ );
+ }
+
+ @override
+ PersonAssetsProvider getProviderOverride(
+ covariant PersonAssetsProvider provider,
+ ) {
+ return call(
+ provider.personId,
+ );
+ }
+
+ static const Iterable? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? get allTransitiveDependencies =>
+ _allTransitiveDependencies;
+
+ @override
+ String? get name => r'personAssetsProvider';
+}
+
+/// See also [personAssets].
+class PersonAssetsProvider extends AutoDisposeFutureProvider {
+ /// See also [personAssets].
+ PersonAssetsProvider(
+ String personId,
+ ) : this._internal(
+ (ref) => personAssets(
+ ref as PersonAssetsRef,
+ personId,
+ ),
+ from: personAssetsProvider,
+ name: r'personAssetsProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$personAssetsHash,
+ dependencies: PersonAssetsFamily._dependencies,
+ allTransitiveDependencies:
+ PersonAssetsFamily._allTransitiveDependencies,
+ personId: personId,
+ );
+
+ PersonAssetsProvider._internal(
+ super._createNotifier, {
+ required super.name,
+ required super.dependencies,
+ required super.allTransitiveDependencies,
+ required super.debugGetCreateSourceHash,
+ required super.from,
+ required this.personId,
+ }) : super.internal();
+
+ final String personId;
+
+ @override
+ Override overrideWith(
+ FutureOr Function(PersonAssetsRef provider) create,
+ ) {
+ return ProviderOverride(
+ origin: this,
+ override: PersonAssetsProvider._internal(
+ (ref) => create(ref as PersonAssetsRef),
+ from: from,
+ name: null,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ debugGetCreateSourceHash: null,
+ personId: personId,
+ ),
+ );
+ }
+
+ @override
+ AutoDisposeFutureProviderElement createElement() {
+ return _PersonAssetsProviderElement(this);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is PersonAssetsProvider && other.personId == personId;
+ }
+
+ @override
+ int get hashCode {
+ var hash = _SystemHash.combine(0, runtimeType.hashCode);
+ hash = _SystemHash.combine(hash, personId.hashCode);
+
+ return _SystemHash.finish(hash);
+ }
+}
+
+mixin PersonAssetsRef on AutoDisposeFutureProviderRef {
+ /// The parameter `personId` of this provider.
+ String get personId;
+}
+
+class _PersonAssetsProviderElement
+ extends AutoDisposeFutureProviderElement with PersonAssetsRef {
+ _PersonAssetsProviderElement(super.provider);
+
+ @override
+ String get personId => (origin as PersonAssetsProvider).personId;
+}
+
+String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd';
+
+/// See also [updatePersonName].
+@ProviderFor(updatePersonName)
+const updatePersonNameProvider = UpdatePersonNameFamily();
+
+/// See also [updatePersonName].
+class UpdatePersonNameFamily extends Family> {
+ /// See also [updatePersonName].
+ const UpdatePersonNameFamily();
+
+ /// See also [updatePersonName].
+ UpdatePersonNameProvider call(
+ String personId,
+ String updatedName,
+ ) {
+ return UpdatePersonNameProvider(
+ personId,
+ updatedName,
+ );
+ }
+
+ @override
+ UpdatePersonNameProvider getProviderOverride(
+ covariant UpdatePersonNameProvider provider,
+ ) {
+ return call(
+ provider.personId,
+ provider.updatedName,
+ );
+ }
+
+ static const Iterable? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? get allTransitiveDependencies =>
+ _allTransitiveDependencies;
+
+ @override
+ String? get name => r'updatePersonNameProvider';
+}
+
+/// See also [updatePersonName].
+class UpdatePersonNameProvider extends AutoDisposeFutureProvider {
+ /// See also [updatePersonName].
+ UpdatePersonNameProvider(
+ String personId,
+ String updatedName,
+ ) : this._internal(
+ (ref) => updatePersonName(
+ ref as UpdatePersonNameRef,
+ personId,
+ updatedName,
+ ),
+ from: updatePersonNameProvider,
+ name: r'updatePersonNameProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$updatePersonNameHash,
+ dependencies: UpdatePersonNameFamily._dependencies,
+ allTransitiveDependencies:
+ UpdatePersonNameFamily._allTransitiveDependencies,
+ personId: personId,
+ updatedName: updatedName,
+ );
+
+ UpdatePersonNameProvider._internal(
+ super._createNotifier, {
+ required super.name,
+ required super.dependencies,
+ required super.allTransitiveDependencies,
+ required super.debugGetCreateSourceHash,
+ required super.from,
+ required this.personId,
+ required this.updatedName,
+ }) : super.internal();
+
+ final String personId;
+ final String updatedName;
+
+ @override
+ Override overrideWith(
+ FutureOr Function(UpdatePersonNameRef provider) create,
+ ) {
+ return ProviderOverride(
+ origin: this,
+ override: UpdatePersonNameProvider._internal(
+ (ref) => create(ref as UpdatePersonNameRef),
+ from: from,
+ name: null,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ debugGetCreateSourceHash: null,
+ personId: personId,
+ updatedName: updatedName,
+ ),
+ );
+ }
+
+ @override
+ AutoDisposeFutureProviderElement createElement() {
+ return _UpdatePersonNameProviderElement(this);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is UpdatePersonNameProvider &&
+ other.personId == personId &&
+ other.updatedName == updatedName;
+ }
+
+ @override
+ int get hashCode {
+ var hash = _SystemHash.combine(0, runtimeType.hashCode);
+ hash = _SystemHash.combine(hash, personId.hashCode);
+ hash = _SystemHash.combine(hash, updatedName.hashCode);
+
+ return _SystemHash.finish(hash);
+ }
+}
+
+mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef {
+ /// The parameter `personId` of this provider.
+ String get personId;
+
+ /// The parameter `updatedName` of this provider.
+ String get updatedName;
+}
+
+class _UpdatePersonNameProviderElement
+ extends AutoDisposeFutureProviderElement with UpdatePersonNameRef {
+ _UpdatePersonNameProviderElement(super.provider);
+
+ @override
+ String get personId => (origin as UpdatePersonNameProvider).personId;
+ @override
+ String get updatedName => (origin as UpdatePersonNameProvider).updatedName;
+}
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart
index 8314ed109..d4cbe0de5 100644
--- a/mobile/lib/modules/search/services/person.service.dart
+++ b/mobile/lib/modules/search/services/person.service.dart
@@ -1,44 +1,40 @@
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
-final personServiceProvider = Provider(
- (ref) => PersonService(
- ref.watch(apiServiceProvider),
- ),
-);
+part 'person.service.g.dart';
+
+@riverpod
+PersonService personService(PersonServiceRef ref) =>
+ PersonService(ref.read(apiServiceProvider));
class PersonService {
+ final Logger _log = Logger("PersonService");
final ApiService _apiService;
PersonService(this._apiService);
- Future?> getCuratedPeople() async {
+ Future> getCuratedPeople() async {
try {
final peopleResponseDto = await _apiService.personApi.getAllPeople();
- return peopleResponseDto?.people;
- } catch (e) {
- debugPrint("Error [getCuratedPeople] ${e.toString()}");
- return null;
+ return peopleResponseDto?.people ?? [];
+ } catch (error, stack) {
+ _log.severe("Error while fetching curated people", error, stack);
+ return [];
}
}
Future?> getPersonAssets(String id) async {
try {
final assets = await _apiService.personApi.getPersonAssets(id);
-
- if (assets == null) {
- return null;
- }
-
- return assets.map((e) => Asset.remote(e)).toList();
- } catch (e) {
- debugPrint("Error [getPersonAssets] ${e.toString()}");
- return null;
+ return assets?.map((e) => Asset.remote(e)).toList();
+ } catch (error, stack) {
+ _log.severe("Error while fetching person assets", error, stack);
}
+ return null;
}
Future updateName(String id, String name) async {
@@ -49,9 +45,9 @@ class PersonService {
name: name,
),
);
- } catch (e) {
- debugPrint("Error [updateName] ${e.toString()}");
- return null;
+ } catch (error, stack) {
+ _log.severe("Error while updating person name", error, stack);
}
+ return null;
}
}
diff --git a/mobile/lib/modules/search/services/person.service.g.dart b/mobile/lib/modules/search/services/person.service.g.dart
new file mode 100644
index 000000000..e66c6c2aa
--- /dev/null
+++ b/mobile/lib/modules/search/services/person.service.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'person.service.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b';
+
+/// See also [personService].
+@ProviderFor(personService)
+final personServiceProvider = AutoDisposeProvider.internal(
+ personService,
+ name: r'personServiceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$personServiceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef PersonServiceRef = AutoDisposeProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart
index aec6188d4..e838c59e1 100644
--- a/mobile/lib/modules/search/ui/curated_people_row.dart
+++ b/mobile/lib/modules/search/ui/curated_people_row.dart
@@ -84,8 +84,7 @@ class CuratedPeopleRow extends StatelessWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"Add name",
- style: TextStyle(
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
),
@@ -98,10 +97,7 @@ class CuratedPeopleRow extends StatelessWidget {
person.label,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 13.0,
- ),
+ style: context.textTheme.labelLarge,
),
),
],
diff --git a/mobile/lib/modules/search/ui/immich_search_bar.dart b/mobile/lib/modules/search/ui/immich_search_bar.dart
index d34a78a77..b3275237f 100644
--- a/mobile/lib/modules/search/ui/immich_search_bar.dart
+++ b/mobile/lib/modules/search/ui/immich_search_bar.dart
@@ -58,10 +58,8 @@ class ImmichSearchBar extends HookConsumerWidget
},
decoration: InputDecoration(
hintText: 'search_bar_hint'.tr(),
- hintStyle: context.textTheme.titleSmall?.copyWith(
- color: context.themeData.colorScheme.onSurface.withOpacity(0.5),
- fontWeight: FontWeight.w500,
- fontSize: 14,
+ hintStyle: context.textTheme.bodyLarge?.copyWith(
+ color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
diff --git a/mobile/lib/modules/search/ui/person_name_edit_form.dart b/mobile/lib/modules/search/ui/person_name_edit_form.dart
index 6e50131f9..e32d4a9e0 100644
--- a/mobile/lib/modules/search/ui/person_name_edit_form.dart
+++ b/mobile/lib/modules/search/ui/person_name_edit_form.dart
@@ -25,6 +25,7 @@ class PersonNameEditForm extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = useTextEditingController(text: personName);
+ final isError = useState(false);
return AlertDialog(
title: const Text(
@@ -37,18 +38,16 @@ class PersonNameEditForm extends HookConsumerWidget {
autofocus: true,
decoration: InputDecoration(
hintText: 'search_page_person_add_name_dialog_hint'.tr(),
+ border: const OutlineInputBorder(),
+ errorText: isError.value ? 'Error occured' : null,
),
),
),
actions: [
TextButton(
- style: TextButton.styleFrom(),
- onPressed: () {
- Navigator.of(context, rootNavigator: true)
- .pop(
- PersonNameEditFormResult(false, ''),
- );
- },
+ onPressed: () => context.pop(
+ PersonNameEditFormResult(false, ''),
+ ),
child: Text(
"search_page_person_add_name_dialog_cancel",
style: TextStyle(
@@ -58,17 +57,15 @@ class PersonNameEditForm extends HookConsumerWidget {
).tr(),
),
TextButton(
- onPressed: () {
- ref.read(
- updatePersonNameProvider(
- UpdatePersonName(personId, controller.text),
- ),
- );
-
- Navigator.of(context, rootNavigator: true)
- .pop(
- PersonNameEditFormResult(true, controller.text),
+ onPressed: () async {
+ isError.value = false;
+ final result = await ref.read(
+ updatePersonNameProvider(personId, controller.text).future,
);
+ isError.value = !result;
+ if (result) {
+ context.pop(PersonNameEditFormResult(true, controller.text));
+ }
},
child: Text(
"search_page_person_add_name_dialog_save",
diff --git a/mobile/lib/modules/search/ui/search_row_title.dart b/mobile/lib/modules/search/ui/search_row_title.dart
index df0f902f9..830bc94c9 100644
--- a/mobile/lib/modules/search/ui/search_row_title.dart
+++ b/mobile/lib/modules/search/ui/search_row_title.dart
@@ -27,16 +27,16 @@ class SearchRowTitle extends StatelessWidget {
children: [
Text(
title,
- style: context.textTheme.titleSmall,
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
),
TextButton(
onPressed: onViewAllPressed,
child: Text(
'search_page_view_all_button',
- style: TextStyle(
+ style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
- fontWeight: FontWeight.bold,
- fontSize: 14.0,
),
).tr(),
),
diff --git a/mobile/lib/modules/search/views/all_motion_videos_page.dart b/mobile/lib/modules/search/views/all_motion_videos_page.dart
index 2ad53ec45..8290f0dd6 100644
--- a/mobile/lib/modules/search/views/all_motion_videos_page.dart
+++ b/mobile/lib/modules/search/views/all_motion_videos_page.dart
@@ -1,10 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class AllMotionPhotosPage extends HookConsumerWidget {
const AllMotionPhotosPage({super.key});
@@ -21,14 +21,10 @@ class AllMotionPhotosPage extends HookConsumerWidget {
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
- body: motionPhotos.when(
- data: (assets) => ImmichAssetGrid(
+ body: motionPhotos.widgetWhen(
+ onData: (assets) => ImmichAssetGrid(
assets: assets,
),
- error: (e, s) => Text(e.toString()),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart
index 3cbedc949..7a8183148 100644
--- a/mobile/lib/modules/search/views/all_people_page.dart
+++ b/mobile/lib/modules/search/views/all_people_page.dart
@@ -1,11 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class AllPeoplePage extends HookConsumerWidget {
const AllPeoplePage({super.key});
@@ -16,34 +15,18 @@ class AllPeoplePage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
- title: Text(
+ title: const Text(
'all_people_page_title',
- style: TextStyle(
- color: context.primaryColor,
- fontWeight: FontWeight.bold,
- fontSize: 16.0,
- ),
).tr(),
leading: IconButton(
onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
- body: curatedPeople.when(
- loading: () => const Center(child: ImmichLoadingIndicator()),
- error: (err, stack) => Center(
- child: Text('Error: $err'),
- ),
- data: (people) => ExploreGrid(
+ body: curatedPeople.widgetWhen(
+ onData: (people) => ExploreGrid(
isPeople: true,
- curatedContent: people
- .map(
- (person) => CuratedContent(
- label: person.name,
- id: person.id,
- ),
- )
- .toList(),
+ curatedContent: people,
),
),
);
diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart
index beb604fd0..683539880 100644
--- a/mobile/lib/modules/search/views/all_videos_page.dart
+++ b/mobile/lib/modules/search/views/all_videos_page.dart
@@ -1,10 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class AllVideosPage extends HookConsumerWidget {
const AllVideosPage({super.key});
@@ -21,14 +21,10 @@ class AllVideosPage extends HookConsumerWidget {
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
- body: videos.when(
- data: (assets) => ImmichAssetGrid(
+ body: videos.widgetWhen(
+ onData: (assets) => ImmichAssetGrid(
assets: assets,
),
- error: (e, s) => Text(e.toString()),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/search/views/curated_location_page.dart b/mobile/lib/modules/search/views/curated_location_page.dart
index d9176e178..6675e0826 100644
--- a/mobile/lib/modules/search/views/curated_location_page.dart
+++ b/mobile/lib/modules/search/views/curated_location_page.dart
@@ -1,11 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
class CuratedLocationPage extends HookConsumerWidget {
@@ -18,25 +18,16 @@ class CuratedLocationPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
- title: Text(
+ title: const Text(
'curated_location_page_title',
- style: TextStyle(
- color: context.primaryColor,
- fontWeight: FontWeight.bold,
- fontSize: 16.0,
- ),
).tr(),
leading: IconButton(
onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
- body: curatedLocation.when(
- loading: () => const Center(child: ImmichLoadingIndicator()),
- error: (err, stack) => Center(
- child: Text('Error: $err'),
- ),
- data: (curatedLocations) => ExploreGrid(
+ body: curatedLocation.widgetWhen(
+ onData: (curatedLocations) => ExploreGrid(
curatedContent: curatedLocations
.map(
(l) => CuratedContent(
diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart
index 2e8637bc7..40a2d1b14 100644
--- a/mobile/lib/modules/search/views/person_result_page.dart
+++ b/mobile/lib/modules/search/views/person_result_page.dart
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
@@ -24,12 +25,12 @@ class PersonResultPage extends HookConsumerWidget {
final name = useState(personName);
showEditNameDialog() {
- showDialog(
+ showDialog(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(
personId: personId,
- personName: personName,
+ personName: name.value,
);
},
).then((result) {
@@ -66,35 +67,33 @@ class PersonResultPage extends HookConsumerWidget {
}
buildTitleBlock() {
- if (name.value == "") {
- return GestureDetector(
- onTap: showEditNameDialog,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'search_page_person_add_name_title',
- style: context.textTheme.titleSmall?.copyWith(
- color: context.themeData.colorScheme.secondary,
- ),
- ).tr(),
- Text(
- 'search_page_person_add_name_subtitle',
- style: context.textTheme.labelSmall,
- ).tr(),
- ],
- ),
- );
- }
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- name.value,
- style: context.textTheme.titleLarge,
- ),
- ],
+ return GestureDetector(
+ onTap: showEditNameDialog,
+ child: name.value.isEmpty
+ ? Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'search_page_person_add_name_title',
+ style: context.textTheme.titleMedium?.copyWith(
+ color: context.primaryColor,
+ ),
+ ).tr(),
+ Text(
+ 'search_page_person_add_name_subtitle',
+ style: context.textTheme.labelLarge,
+ ).tr(),
+ ],
+ )
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ name.value,
+ style: context.textTheme.titleLarge,
+ ),
+ ],
+ ),
);
}
@@ -112,41 +111,31 @@ class PersonResultPage extends HookConsumerWidget {
),
],
),
- body: ref.watch(personAssetsProvider(personId)).when(
- loading: () => const Center(child: CircularProgressIndicator()),
- error: (error, stackTrace) => Center(
- child: Text(
- error.toString(),
- ),
- ),
- data: (data) => data.isEmpty
- ? const Center(
- child: Text('Opps'),
- )
- : ImmichAssetGrid(
- renderList: data,
- topWidget: Padding(
- padding: const EdgeInsets.only(left: 8.0, top: 24),
- child: Row(
- children: [
- CircleAvatar(
- radius: 36,
- backgroundImage: NetworkImage(
- getFaceThumbnailUrl(personId),
- headers: {
- "Authorization":
- "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}",
- },
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 16.0),
- child: buildTitleBlock(),
- ),
- ],
+ body: ref.watch(personAssetsProvider(personId)).widgetWhen(
+ onData: (renderList) => ImmichAssetGrid(
+ renderList: renderList,
+ topWidget: Padding(
+ padding: const EdgeInsets.only(left: 8.0, top: 24),
+ child: Row(
+ children: [
+ CircleAvatar(
+ radius: 36,
+ backgroundImage: NetworkImage(
+ getFaceThumbnailUrl(personId),
+ headers: {
+ "Authorization":
+ "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}",
+ },
),
),
- ),
+ Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: buildTitleBlock(),
+ ),
+ ],
+ ),
+ ),
+ ),
),
);
}
diff --git a/mobile/lib/modules/search/views/recently_added_page.dart b/mobile/lib/modules/search/views/recently_added_page.dart
index 55e920615..538dea3d7 100644
--- a/mobile/lib/modules/search/views/recently_added_page.dart
+++ b/mobile/lib/modules/search/views/recently_added_page.dart
@@ -1,10 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class RecentlyAddedPage extends HookConsumerWidget {
const RecentlyAddedPage({super.key});
@@ -21,14 +21,10 @@ class RecentlyAddedPage extends HookConsumerWidget {
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
- body: recents.when(
- data: (searchResponse) => ImmichAssetGrid(
+ body: recents.widgetWhen(
+ onData: (searchResponse) => ImmichAssetGrid(
assets: searchResponse,
),
- error: (e, s) => Text(e.toString()),
- loading: () => const Center(
- child: ImmichLoadingIndicator(),
- ),
),
);
}
diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart
index 562e42c32..fb4bd4979 100644
--- a/mobile/lib/modules/search/views/search_page.dart
+++ b/mobile/lib/modules/search/views/search_page.dart
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
@@ -15,7 +16,7 @@ import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
// ignore: must_be_immutable
class SearchPage extends HookConsumerWidget {
@@ -33,8 +34,8 @@ class SearchPage extends HookConsumerWidget {
double imageSize = math.min(context.width / 3, 150);
TextStyle categoryTitleStyle = const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 14.0,
+ fontWeight: FontWeight.w500,
+ fontSize: 15.0,
);
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
@@ -73,19 +74,10 @@ class SearchPage extends HookConsumerWidget {
buildPeople() {
return SizedBox(
height: imageSize,
- child: curatedPeople.when(
- loading: () => const Center(child: ImmichLoadingIndicator()),
- error: (err, stack) => Center(child: Text('Error: $err')),
- data: (people) => CuratedPeopleRow(
- content: people
- .map(
- (person) => CuratedContent(
- id: person.id,
- label: person.name,
- ),
- )
- .take(12)
- .toList(),
+ child: curatedPeople.widgetWhen(
+ onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+ onData: (people) => CuratedPeopleRow(
+ content: people.take(12).toList(),
onTap: (content, index) {
context.autoPush(
PersonResultRoute(
@@ -105,10 +97,9 @@ class SearchPage extends HookConsumerWidget {
buildPlaces() {
return SizedBox(
height: imageSize,
- child: curatedLocation.when(
- loading: () => const Center(child: ImmichLoadingIndicator()),
- error: (err, stack) => Center(child: Text('Error: $err')),
- data: (locations) => CuratedPlacesRow(
+ child: curatedLocation.widgetWhen(
+ onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+ onData: (locations) => CuratedPlacesRow(
isMapEnabled: isMapEnabled,
content: locations
.map(
@@ -164,7 +155,9 @@ class SearchPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'search_page_your_activity',
- style: context.textTheme.titleSmall,
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
),
ListTile(
@@ -194,11 +187,15 @@ class SearchPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'search_page_categories',
- style: context.textTheme.titleSmall,
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
),
ListTile(
- title: Text('search_page_screenshots', style: categoryTitleStyle).tr(),
+ title:
+ Text('search_page_screenshots', style: categoryTitleStyle)
+ .tr(),
leading: Icon(
Icons.screenshot,
color: categoryIconColor,
@@ -263,7 +260,7 @@ class CategoryDivider extends StatelessWidget {
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(
- left: 72,
+ left: 56,
right: 16,
),
child: Divider(
diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.dart b/mobile/lib/modules/settings/providers/app_settings.provider.dart
index f5d172e4c..96991451f 100644
--- a/mobile/lib/modules/settings/providers/app_settings.provider.dart
+++ b/mobile/lib/modules/settings/providers/app_settings.provider.dart
@@ -1,4 +1,8 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
-final appSettingsServiceProvider = Provider((ref) => AppSettingsService());
+part 'app_settings.provider.g.dart';
+
+@Riverpod(keepAlive: true)
+AppSettingsService appSettingsService(AppSettingsServiceRef ref) =>
+ AppSettingsService();
diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.g.dart b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart
new file mode 100644
index 000000000..692dcf7c0
--- /dev/null
+++ b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart
@@ -0,0 +1,26 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'app_settings.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$appSettingsServiceHash() =>
+ r'957a65af6967701112f3076b507f9738fec4b7be';
+
+/// See also [appSettingsService].
+@ProviderFor(appSettingsService)
+final appSettingsServiceProvider = Provider.internal(
+ appSettingsService,
+ name: r'appSettingsServiceProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$appSettingsServiceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef AppSettingsServiceRef = ProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart b/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart
index e07c30dae..d0397fe5a 100644
--- a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart
+++ b/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart
@@ -45,17 +45,12 @@ class AdvancedSettings extends HookConsumerWidget {
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
"advanced_settings_tile_title",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
"advanced_settings_tile_subtitle",
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: [
SettingsSwitchListTile(
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
index 856935ccb..aa3123f3d 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
@@ -54,8 +54,7 @@ class LayoutSettings extends HookConsumerWidget {
activeColor: context.primaryColor,
title: Text(
"asset_list_layout_settings_dynamic_layout_title",
- style: context.textTheme.labelLarge
- ?.copyWith(fontWeight: FontWeight.bold),
+ style: context.textTheme.labelLarge,
).tr(),
onChanged: switchChanged,
value: useDynamicLayout.value,
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart
index a7a2f8b95..c3e0fc8eb 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart
@@ -14,17 +14,12 @@ class AssetListSettings extends StatelessWidget {
Widget build(BuildContext context) {
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
'asset_list_settings_title',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
'asset_list_settings_subtitle',
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: const [
TilesPerRow(),
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
index 50896fe0b..b94635a30 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
@@ -37,8 +37,7 @@ class StorageIndicator extends HookConsumerWidget {
activeColor: context.primaryColor,
title: Text(
"theme_setting_asset_list_storage_indicator_title",
- style:
- context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
+ style: context.textTheme.labelLarge,
).tr(),
onChanged: switchChanged,
value: showStorageIndicator.value,
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
index 5693e2a5f..e2f245582 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
@@ -37,12 +37,9 @@ class TilesPerRow extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
- title: const Text(
+ title: Text(
"theme_setting_asset_list_tiles_per_row_title",
- style: TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.labelLarge,
).tr(args: ["${itemsValue.value.toInt()}"]),
),
Slider(
diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart
index d0b86a186..5c8620473 100644
--- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart
+++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart
@@ -28,22 +28,19 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
'theme_setting_image_viewer_quality_title',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
'theme_setting_image_viewer_quality_subtitle',
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: [
ListTile(
- title: const Text('setting_image_viewer_help').tr(),
- dense: true,
+ title: Text(
+ 'setting_image_viewer_help',
+ style: context.textTheme.bodyMedium,
+ ).tr(),
),
SettingsSwitchListTile(
appSettingService: settings,
diff --git a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart b/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart
index df1bcbdf7..a64da0481 100644
--- a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart
+++ b/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart
@@ -27,30 +27,21 @@ class LocalStorageSettings extends HookConsumerWidget {
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
"cache_settings_tile_title",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
"cache_settings_tile_subtitle",
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: [
ListTile(
title: Text(
"cache_settings_duplicated_assets_title",
- style: context.textTheme.labelLarge
- ?.copyWith(fontWeight: FontWeight.bold),
+ style: context.textTheme.titleSmall,
).tr(args: ["${cacheItemCount.value}"]),
subtitle: const Text(
"cache_settings_duplicated_assets_subtitle",
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
trailing: TextButton(
onPressed: cacheItemCount.value > 0 ? clearCache : null,
diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
index 747c541d4..e996ad1ea 100644
--- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
+++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
@@ -67,17 +67,12 @@ class NotificationSetting extends HookConsumerWidget {
final String formattedValue = _formatSliderValue(sliderValue.value);
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
'setting_notifications_title',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
'setting_notifications_subtitle',
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: [
if (!hasPermission)
diff --git a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart
index e66e6319c..b5277b9c1 100644
--- a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart
+++ b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart
@@ -40,10 +40,14 @@ class SettingsSwitchListTile extends StatelessWidget {
dense: true,
title: Text(
title,
- style:
- context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
+ style: context.textTheme.titleSmall,
),
- subtitle: subtitle != null ? Text(subtitle!) : null,
+ subtitle: subtitle != null
+ ? Text(
+ subtitle!,
+ style: context.textTheme.bodyMedium,
+ )
+ : null,
);
}
}
diff --git a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart b/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart
index fd25873cd..0657fa7e0 100644
--- a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart
+++ b/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart
@@ -26,17 +26,12 @@ class ThemeSetting extends HookConsumerWidget {
return ExpansionTile(
textColor: context.primaryColor,
- title: const Text(
+ title: Text(
'theme_setting_theme_title',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
+ style: context.textTheme.titleMedium,
).tr(),
subtitle: const Text(
'theme_setting_theme_subtitle',
- style: TextStyle(
- fontSize: 13,
- ),
).tr(),
children: [
SwitchListTile.adaptive(
diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart
index e0547d0d6..0a1651de9 100644
--- a/mobile/lib/modules/settings/views/settings_page.dart
+++ b/mobile/lib/modules/settings/views/settings_page.dart
@@ -1,6 +1,7 @@
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/modules/settings/ui/advanced_settings/advanced_settings.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
@@ -18,19 +19,13 @@ class SettingsPage extends HookConsumerWidget {
leading: IconButton(
iconSize: 20,
splashRadius: 24,
- onPressed: () {
- Navigator.pop(context);
- },
+ onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
automaticallyImplyLeading: false,
centerTitle: false,
title: const Text(
'setting_pages_app_bar_settings',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
- ),
).tr(),
),
body: ListView(
diff --git a/mobile/lib/modules/shared_link/ui/shared_link_item.dart b/mobile/lib/modules/shared_link/ui/shared_link_item.dart
index 5b14c4ab1..8605bdeaf 100644
--- a/mobile/lib/modules/shared_link/ui/shared_link_item.dart
+++ b/mobile/lib/modules/shared_link/ui/shared_link_item.dart
@@ -1,4 +1,5 @@
import 'dart:math' as math;
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
@@ -26,13 +27,13 @@ class SharedLinkItem extends ConsumerWidget {
}
Widget getExpiryDuration(bool isDarkMode) {
- var expiresText = "Expires ∞";
+ var expiresText = "shared_link_expires_never".tr();
if (sharedLink.expiresAt != null) {
if (isExpired()) {
return Text(
- "Expired",
+ "shared_link_expired",
style: TextStyle(color: Colors.red[300]),
- );
+ ).tr();
}
final difference = sharedLink.expiresAt!.difference(DateTime.now());
debugPrint("Difference: $difference");
@@ -41,13 +42,15 @@ class SharedLinkItem extends ConsumerWidget {
if (difference.inHours % 24 > 12) {
dayDifference += 1;
}
- expiresText = "in $dayDifference days";
+ expiresText = "shared_link_expires_days".plural(dayDifference);
} else if (difference.inHours > 0) {
- expiresText = "in ${difference.inHours} hours";
+ expiresText = "shared_link_expires_hours".plural(difference.inHours);
} else if (difference.inMinutes > 0) {
- expiresText = "in ${difference.inMinutes} minutes";
+ expiresText =
+ "shared_link_expires_minutes".plural(difference.inMinutes);
} else if (difference.inSeconds > 0) {
- expiresText = "in ${difference.inSeconds} seconds";
+ expiresText =
+ "shared_link_expires_seconds".plural(difference.inSeconds);
}
}
return Text(
@@ -72,7 +75,7 @@ class SharedLinkItem extends ConsumerWidget {
context: context,
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
- msg: 'Cannot fetch the server url',
+ msg: "shared_link_error_server_url_fetch".tr(),
);
return;
}
@@ -83,11 +86,14 @@ class SharedLinkItem extends ConsumerWidget {
),
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
+ SnackBar(
content: Text(
- "Copied to clipboard",
- ),
- duration: Duration(seconds: 2),
+ "shared_link_clipboard_copied_massage",
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
+ ).tr(),
+ duration: const Duration(seconds: 2),
),
);
});
@@ -148,8 +154,8 @@ class SharedLinkItem extends ConsumerWidget {
label: Text(
labelText,
style: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.bold,
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.black : Colors.white,
),
),
@@ -163,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget {
Widget buildBottomInfo() {
return Row(
children: [
- if (sharedLink.allowUpload) buildInfoChip("Upload"),
- if (sharedLink.allowDownload) buildInfoChip("Download"),
- if (sharedLink.showMetadata) buildInfoChip("EXIF"),
+ if (sharedLink.allowUpload)
+ buildInfoChip("shared_link_info_chip_upload".tr()),
+ if (sharedLink.allowDownload)
+ buildInfoChip("shared_link_info_chip_download".tr()),
+ if (sharedLink.showMetadata)
+ buildInfoChip("shared_link_info_chip_metadata".tr()),
],
);
}
diff --git a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
index df14ba3d9..fe2212cc2 100644
--- a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
+++ b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
@@ -275,7 +275,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: const Text("shared_link_clipboard_copied_massage").tr(),
+ content: Text(
+ "shared_link_clipboard_copied_massage",
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
+ ).tr(),
duration: const Duration(seconds: 2),
),
);
diff --git a/mobile/lib/modules/shared_link/views/shared_link_page.dart b/mobile/lib/modules/shared_link/views/shared_link_page.dart
index 04f57a48b..7638441b1 100644
--- a/mobile/lib/modules/shared_link/views/shared_link_page.dart
+++ b/mobile/lib/modules/shared_link/views/shared_link_page.dart
@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class SharedLinkPage extends HookConsumerWidget {
const SharedLinkPage({Key? key}) : super(key: key);
@@ -18,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget {
useEffect(
() {
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
- return () => ref.invalidate(sharedLinksStateProvider);
+ return () {
+ if (!context.mounted) return;
+ ref.invalidate(sharedLinksStateProvider);
+ };
},
[],
);
@@ -67,12 +70,10 @@ class SharedLinkPage extends HookConsumerWidget {
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0),
- child: const Text(
+ child: Text(
"shared_link_manage_links",
- style: TextStyle(
- fontSize: 14,
- color: Colors.grey,
- fontWeight: FontWeight.bold,
+ style: context.textTheme.labelLarge?.copyWith(
+ color: context.textTheme.labelLarge?.color?.withAlpha(200),
),
).tr(),
),
@@ -115,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget {
centerTitle: false,
),
body: SafeArea(
- child: sharedLinks.when(
- data: (links) =>
+ child: sharedLinks.widgetWhen(
+ onError: (error, stackTrace) => buildNoShares(),
+ onData: (links) =>
links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
- error: (error, stackTrace) => buildNoShares(),
- loading: () => const Center(child: ImmichLoadingIndicator()),
),
),
);
diff --git a/mobile/lib/modules/trash/views/trash_page.dart b/mobile/lib/modules/trash/views/trash_page.dart
index ad365189e..88fd32d01 100644
--- a/mobile/lib/modules/trash/views/trash_page.dart
+++ b/mobile/lib/modules/trash/views/trash_page.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@@ -11,8 +12,8 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
class TrashPage extends HookConsumerWidget {
const TrashPage({super.key});
@@ -24,7 +25,7 @@ class TrashPage extends HookConsumerWidget {
ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
final selectionEnabledHook = useState(false);
final selection = useState({});
- final processing = useState(false);
+ final processing = useProcessingOverlay();
void selectionListener(
bool multiselect,
@@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget {
);
}
- return trashedAssets.when(
- loading: () => Scaffold(
- appBar: buildAppBar("?"),
- body: const Center(child: CircularProgressIndicator()),
+ return Scaffold(
+ appBar: trashedAssets.maybeWhen(
+ orElse: () => buildAppBar("?"),
+ data: (data) => buildAppBar(data.totalAssets.toString()),
),
- error: (error, stackTrace) => Scaffold(
- appBar: buildAppBar("!"),
- body: Center(child: Text(error.toString())),
- ),
- data: (data) => Scaffold(
- appBar: buildAppBar(data.totalAssets.toString()),
- body: data.isEmpty
+ body: trashedAssets.widgetWhen(
+ onData: (data) => data.isEmpty
? Center(
child: Text('trash_page_no_assets'.tr()),
)
@@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget {
showMultiSelectIndicator: false,
showStack: true,
topWidget: Padding(
- padding: const EdgeInsets.only(
- top: 24,
- bottom: 24,
- left: 12,
- right: 12,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 24,
),
child: const Text(
"trash_page_info",
@@ -267,8 +261,6 @@ class TrashPage extends HookConsumerWidget {
),
),
if (selectionEnabledHook.value) buildBottomBar(),
- if (processing.value)
- const Center(child: ImmichLoadingIndicator()),
],
),
),
diff --git a/mobile/lib/shared/providers/api.provider.dart b/mobile/lib/shared/providers/api.provider.dart
index 24cf864e0..cc73f02b3 100644
--- a/mobile/lib/shared/providers/api.provider.dart
+++ b/mobile/lib/shared/providers/api.provider.dart
@@ -1,4 +1,7 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
-final apiServiceProvider = Provider((ref) => ApiService());
+part 'api.provider.g.dart';
+
+@Riverpod(keepAlive: true)
+ApiService apiService(ApiServiceRef ref) => ApiService();
diff --git a/mobile/lib/shared/providers/api.provider.g.dart b/mobile/lib/shared/providers/api.provider.g.dart
new file mode 100644
index 000000000..4bc7e93d1
--- /dev/null
+++ b/mobile/lib/shared/providers/api.provider.g.dart
@@ -0,0 +1,24 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d';
+
+/// See also [apiService].
+@ProviderFor(apiService)
+final apiServiceProvider = Provider.internal(
+ apiService,
+ name: r'apiServiceProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product') ? null : _$apiServiceHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef ApiServiceRef = ProviderRef;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
index 60ee135b8..053770a92 100644
--- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
+++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
@@ -54,7 +54,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
color: context.primaryColor,
- fontSize: 15,
+ fontSize: 16,
),
),
),
@@ -72,14 +72,15 @@ class ImmichAppBarDialog extends HookConsumerWidget {
leading: SizedBox(
child: Icon(
icon,
- color: theme.textTheme.labelMedium?.color,
+ color: theme.textTheme.labelLarge?.color?.withAlpha(250),
size: 20,
),
),
title: Text(
text,
- style:
- theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
+ style: theme.textTheme.labelLarge?.copyWith(
+ color: theme.textTheme.labelLarge?.color?.withAlpha(250),
+ ),
).tr(),
onTap: onTap,
);
@@ -145,9 +146,11 @@ class ImmichAppBarDialog extends HookConsumerWidget {
Icons.storage_rounded,
color: theme.primaryColor,
),
- title: const Text(
+ title: Text(
"backup_controller_page_server_storage",
- style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+ style: context.textTheme.labelLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ ),
).tr(),
isThreeLine: true,
subtitle: Padding(
@@ -230,7 +233,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
clipBehavior: Clip.hardEdge,
alignment: Alignment.topCenter,
insetPadding: EdgeInsets.only(
- top: isHorizontal ? 20 : 60,
+ top: isHorizontal ? 20 : 40,
left: horizontalPadding,
right: horizontalPadding,
bottom: isHorizontal ? 20 : 100,
diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart
index abb81ca89..baae732b1 100644
--- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart
+++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart
@@ -31,8 +31,8 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
}
final userImage = UserCircleAvatar(
- radius: 20,
- size: 40,
+ radius: 22,
+ size: 44,
user: user,
);
@@ -119,16 +119,15 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
),
title: Text(
authState.name,
- style: TextStyle(
+ style: context.textTheme.titleMedium?.copyWith(
color: context.primaryColor,
- fontWeight: FontWeight.bold,
- fontSize: 16,
+ fontWeight: FontWeight.w500,
),
),
subtitle: Text(
authState.userEmail,
- style: context.textTheme.labelMedium?.copyWith(
- fontSize: 12,
+ style: context.textTheme.bodySmall?.copyWith(
+ color: context.textTheme.bodySmall?.color?.withAlpha(200),
),
),
),
diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
index ec8fb0911..40b43838e 100644
--- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
+++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart
@@ -18,6 +18,8 @@ class AppBarServerInfo extends HookConsumerWidget {
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final appInfo = useState({});
+ const titleFontSize = 12.0;
+ const contentFontSize = 11.0;
getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
@@ -63,7 +65,7 @@ class AppBarServerInfo extends HookConsumerWidget {
style: TextStyle(
fontSize: 11,
color: context.primaryColor,
- fontWeight: FontWeight.w600,
+ fontWeight: FontWeight.w500,
),
),
),
@@ -83,9 +85,9 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Text(
"server_info_box_app_version".tr(),
style: TextStyle(
- fontSize: 11,
+ fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
),
@@ -97,7 +99,7 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Text(
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
style: TextStyle(
- fontSize: 11,
+ fontSize: contentFontSize,
color: context.textTheme.labelSmall?.color
?.withOpacity(0.5),
fontWeight: FontWeight.bold,
@@ -123,9 +125,9 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Text(
"server_info_box_server_version".tr(),
style: TextStyle(
- fontSize: 11,
+ fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
),
@@ -139,7 +141,7 @@ class AppBarServerInfo extends HookConsumerWidget {
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
: "--",
style: TextStyle(
- fontSize: 11,
+ fontSize: contentFontSize,
color: context.textTheme.labelSmall?.color
?.withOpacity(0.5),
fontWeight: FontWeight.bold,
@@ -165,9 +167,9 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Text(
"server_info_box_server_url".tr(),
style: TextStyle(
- fontSize: 11,
+ fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
),
@@ -194,7 +196,7 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Text(
getServerUrl() ?? '--',
style: TextStyle(
- fontSize: 11,
+ fontSize: contentFontSize,
color: context.textTheme.labelSmall?.color
?.withOpacity(0.5),
fontWeight: FontWeight.bold,
@@ -234,9 +236,9 @@ class AppBarServerInfo extends HookConsumerWidget {
Text(
"server_info_box_latest_release".tr(),
style: TextStyle(
- fontSize: 11,
+ fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w500,
),
),
],
@@ -252,7 +254,7 @@ class AppBarServerInfo extends HookConsumerWidget {
? "${serverInfoState.latestVersion.major}.${serverInfoState.latestVersion.minor}.${serverInfoState.latestVersion.patch}"
: "--",
style: TextStyle(
- fontSize: 11,
+ fontSize: contentFontSize,
color: context.textTheme.labelSmall?.color
?.withOpacity(0.5),
fontWeight: FontWeight.bold,
diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart
index db5dd3c19..24eedcd47 100644
--- a/mobile/lib/shared/ui/immich_loading_indicator.dart
+++ b/mobile/lib/shared/ui/immich_loading_indicator.dart
@@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
padding: const EdgeInsets.all(15),
child: const CircularProgressIndicator(
color: Colors.white,
- strokeWidth: 2,
+ strokeWidth: 3,
),
);
}
diff --git a/mobile/lib/shared/ui/scaffold_error_body.dart b/mobile/lib/shared/ui/scaffold_error_body.dart
new file mode 100644
index 000000000..fef6bef59
--- /dev/null
+++ b/mobile/lib/shared/ui/scaffold_error_body.dart
@@ -0,0 +1,36 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+
+// Error widget to be used in Scaffold when an AsyncError is received
+class ScaffoldErrorBody extends StatelessWidget {
+ final bool withIcon;
+
+ const ScaffoldErrorBody({super.key, this.withIcon = true});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "scaffold_body_error_occured",
+ style: context.textTheme.displayMedium,
+ textAlign: TextAlign.center,
+ ).tr(),
+ if (withIcon)
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 15),
+ child: Icon(
+ Icons.error_outline,
+ size: 100,
+ color: context.themeData.iconTheme.color?.withOpacity(0.5),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/mobile/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart
index f8ddf5191..8b737fb5c 100644
--- a/mobile/lib/shared/views/app_log_detail_page.dart
+++ b/mobile/lib/shared/views/app_log_detail_page.dart
@@ -39,7 +39,14 @@ class AppLogDetailPage extends HookConsumerWidget {
Clipboard.setData(ClipboardData(text: stackTrace))
.then((_) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("Copied to clipboard")),
+ SnackBar(
+ content: Text(
+ "Copied to clipboard",
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
+ ),
+ ),
);
});
},
@@ -98,7 +105,14 @@ class AppLogDetailPage extends HookConsumerWidget {
onPressed: () {
Clipboard.setData(ClipboardData(text: message)).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("Copied to clipboard")),
+ SnackBar(
+ content: Text(
+ "Copied to clipboard",
+ style: context.textTheme.bodyLarge?.copyWith(
+ color: context.primaryColor,
+ ),
+ ),
+ ),
);
});
},
diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart
index 6e4ef166b..85f0123ed 100644
--- a/mobile/lib/shared/views/immich_loading_overlay.dart
+++ b/mobile/lib/shared/views/immich_loading_overlay.dart
@@ -1,41 +1,64 @@
import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
-class ImmichLoadingOverlay extends StatelessWidget {
- const ImmichLoadingOverlay({
- Key? key,
- }) : super(key: key);
+final _loadingEntry = OverlayEntry(
+ builder: (context) => SizedBox.square(
+ dimension: double.infinity,
+ child: DecoratedBox(
+ decoration:
+ BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
+ child: const Center(child: ImmichLoadingIndicator()),
+ ),
+ ),
+);
+
+ValueNotifier useProcessingOverlay() {
+ return use(const _LoadingOverlay());
+}
+
+class _LoadingOverlay extends Hook> {
+ const _LoadingOverlay();
@override
- Widget build(BuildContext context) {
- return ValueListenableBuilder(
- valueListenable:
- ImmichLoadingOverlayController.appLoader.loaderShowingNotifier,
- builder: (context, shouldShow, child) {
- return shouldShow
- ? const Scaffold(
- backgroundColor: Colors.black54,
- body: Center(
- child: ImmichLoadingIndicator(),
- ),
- )
- : const SizedBox();
- },
- );
- }
+ _LoadingOverlayState createState() => _LoadingOverlayState();
}
-class ImmichLoadingOverlayController {
- static final ImmichLoadingOverlayController appLoader =
- ImmichLoadingOverlayController();
- ValueNotifier loaderShowingNotifier = ValueNotifier(false);
- ValueNotifier loaderTextNotifier = ValueNotifier('error message');
+class _LoadingOverlayState
+ extends HookState, _LoadingOverlay> {
+ late final _isProcessing = ValueNotifier(false)..addListener(_listener);
+ OverlayEntry? overlayEntry;
- void show() {
- loaderShowingNotifier.value = true;
+ void _listener() {
+ setState(() {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (_isProcessing.value) {
+ overlayEntry?.remove();
+ overlayEntry = _loadingEntry;
+ Overlay.of(context).insert(_loadingEntry);
+ } else {
+ overlayEntry?.remove();
+ overlayEntry = null;
+ }
+ });
+ });
}
- void hide() {
- loaderShowingNotifier.value = false;
+ @override
+ ValueNotifier build(BuildContext context) {
+ return _isProcessing;
}
+
+ @override
+ void dispose() {
+ _isProcessing.dispose();
+ super.dispose();
+ }
+
+ @override
+ Object? get debugValue => _isProcessing.value;
+
+ @override
+ String get debugLabel => 'useProcessingOverlay<>';
}
diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart
index 670a7660d..4313da60f 100644
--- a/mobile/lib/utils/immich_app_theme.dart
+++ b/mobile/lib/utils/immich_app_theme.dart
@@ -38,19 +38,19 @@ ThemeData immichLightTheme = ThemeData(
hintColor: Colors.indigo,
focusColor: Colors.indigo,
splashColor: Colors.indigo.withOpacity(0.15),
- fontFamily: 'WorkSans',
+ fontFamily: 'Overpass',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(
- fontFamily: 'WorkSans',
+ fontFamily: 'Overpass',
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
backgroundColor: Colors.white,
),
- appBarTheme: AppBarTheme(
- titleTextStyle: const TextStyle(
- fontFamily: 'WorkSans',
+ appBarTheme: const AppBarTheme(
+ titleTextStyle: TextStyle(
+ fontFamily: 'Overpass',
color: Colors.indigo,
fontWeight: FontWeight.bold,
fontSize: 18,
@@ -61,7 +61,7 @@ ThemeData immichLightTheme = ThemeData(
scrolledUnderElevation: 0,
centerTitle: true,
),
- bottomNavigationBarTheme: BottomNavigationBarThemeData(
+ bottomNavigationBarTheme: const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
@@ -69,7 +69,7 @@ ThemeData immichLightTheme = ThemeData(
cardTheme: const CardTheme(
surfaceTintColor: Colors.transparent,
),
- drawerTheme: DrawerThemeData(
+ drawerTheme: const DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
@@ -125,9 +125,9 @@ ThemeData immichLightTheme = ThemeData(
surfaceTintColor: Colors.transparent,
labelTextStyle: MaterialStatePropertyAll(
TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w600,
- color: Colors.grey[700],
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: Colors.grey[800],
),
),
),
@@ -160,10 +160,10 @@ ThemeData immichDarkTheme = ThemeData(
primaryColor: immichDarkThemePrimaryColor,
scaffoldBackgroundColor: immichDarkBackgroundColor,
hintColor: Colors.grey[600],
- fontFamily: 'WorkSans',
+ fontFamily: 'Overpass',
snackBarTheme: SnackBarThemeData(
- contentTextStyle: TextStyle(
- fontFamily: 'WorkSans',
+ contentTextStyle: const TextStyle(
+ fontFamily: 'Overpass',
color: immichDarkThemePrimaryColor,
fontWeight: FontWeight.bold,
),
@@ -174,35 +174,35 @@ ThemeData immichDarkTheme = ThemeData(
foregroundColor: immichDarkThemePrimaryColor,
),
),
- appBarTheme: AppBarTheme(
+ appBarTheme: const AppBarTheme(
titleTextStyle: TextStyle(
- fontFamily: 'WorkSans',
+ fontFamily: 'Overpass',
color: immichDarkThemePrimaryColor,
fontWeight: FontWeight.bold,
fontSize: 18,
),
- backgroundColor: const Color.fromARGB(255, 32, 33, 35),
+ backgroundColor: Color.fromARGB(255, 32, 33, 35),
foregroundColor: immichDarkThemePrimaryColor,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
- bottomNavigationBarTheme: BottomNavigationBarThemeData(
+ bottomNavigationBarTheme: const BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
- backgroundColor: const Color.fromARGB(255, 35, 36, 37),
+ backgroundColor: Color.fromARGB(255, 35, 36, 37),
selectedItemColor: immichDarkThemePrimaryColor,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichDarkBackgroundColor,
scrimColor: Colors.white.withOpacity(0.1),
),
- textTheme: TextTheme(
- displayLarge: const TextStyle(
+ textTheme: const TextTheme(
+ displayLarge: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
),
- displayMedium: const TextStyle(
+ displayMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
@@ -212,15 +212,15 @@ ThemeData immichDarkTheme = ThemeData(
fontWeight: FontWeight.bold,
color: immichDarkThemePrimaryColor,
),
- titleSmall: const TextStyle(
+ titleSmall: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
- titleMedium: const TextStyle(
+ titleMedium: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
- titleLarge: const TextStyle(
+ titleLarge: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold,
),
@@ -249,16 +249,16 @@ ThemeData immichDarkTheme = ThemeData(
surfaceTintColor: Colors.transparent,
labelTextStyle: MaterialStatePropertyAll(
TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w600,
- color: Colors.grey[500],
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: Colors.grey[300],
),
),
),
dialogTheme: const DialogTheme(
surfaceTintColor: Colors.transparent,
),
- inputDecorationTheme: InputDecorationTheme(
+ inputDecorationTheme: const InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: immichDarkThemePrimaryColor,
@@ -267,12 +267,12 @@ ThemeData immichDarkTheme = ThemeData(
labelStyle: TextStyle(
color: immichDarkThemePrimaryColor,
),
- hintStyle: const TextStyle(
+ hintStyle: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
- textSelectionTheme: TextSelectionThemeData(
+ textSelectionTheme: const TextSelectionThemeData(
cursorColor: immichDarkThemePrimaryColor,
),
);
diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart
index b771a6f70..95fcd8078 100644
--- a/mobile/lib/utils/url_helper.dart
+++ b/mobile/lib/utils/url_helper.dart
@@ -3,10 +3,10 @@ import 'package:immich_mobile/shared/models/store.dart';
String sanitizeUrl(String url) {
// Add schema if none is set
final urlWithSchema =
- url.startsWith(RegExp(r"https?://")) ? url : "https://$url";
+ url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url";
// Remove trailing slash(es)
- return urlWithSchema.replaceFirst(RegExp(r"/+$"), "");
+ return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), "");
}
String? getServerUrl() {
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index 10f10fb01..f54b788a4 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -46,7 +46,6 @@ doc/CQMode.md
doc/ChangePasswordDto.md
doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md
-doc/CitiesFile.md
doc/ClassificationConfig.md
doc/Colorspace.md
doc/CreateAlbumDto.md
@@ -231,7 +230,6 @@ lib/model/bulk_ids_dto.dart
lib/model/change_password_dto.dart
lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart
-lib/model/cities_file.dart
lib/model/classification_config.dart
lib/model/clip_config.dart
lib/model/clip_mode.dart
@@ -388,7 +386,6 @@ test/bulk_ids_dto_test.dart
test/change_password_dto_test.dart
test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart
-test/cities_file_test.dart
test/classification_config_test.dart
test/clip_config_test.dart
test/clip_mode_test.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 1de4ac8ad..42680e679 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 1.87.0
+- API version: 1.89.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -98,6 +98,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
*AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
+*AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
*AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics |
@@ -110,7 +111,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random |
*AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket |
*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
-*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
+*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release
*AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore |
*AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
@@ -243,7 +244,6 @@ Class | Method | HTTP request | Description
- [ChangePasswordDto](doc//ChangePasswordDto.md)
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
- - [CitiesFile](doc//CitiesFile.md)
- [ClassificationConfig](doc//ClassificationConfig.md)
- [Colorspace](doc//Colorspace.md)
- [CreateAlbumDto](doc//CreateAlbumDto.md)
diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md
index 16f7ef94d..b479c08f3 100644
--- a/mobile/openapi/doc/AssetApi.md
+++ b/mobile/openapi/doc/AssetApi.md
@@ -16,6 +16,7 @@ Method | HTTP request | Description
[**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
[**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
+[**getAllUserAssetsByDeviceId**](AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
[**getAssetStatistics**](AssetApi.md#getassetstatistics) | **GET** /asset/statistics |
@@ -28,7 +29,7 @@ Method | HTTP request | Description
[**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random |
[**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket |
[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
-[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
+[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release
[**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore |
[**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore |
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
@@ -443,6 +444,63 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+# **getAllUserAssetsByDeviceId**
+> List getAllUserAssetsByDeviceId(deviceId)
+
+
+
+Get all asset of a device that are in the database, ID only.
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = AssetApi();
+final deviceId = deviceId_example; // String |
+
+try {
+ final result = api_instance.getAllUserAssetsByDeviceId(deviceId);
+ print(result);
+} catch (e) {
+ print('Exception when calling AssetApi->getAllUserAssetsByDeviceId: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **deviceId** | **String**| |
+
+### Return type
+
+**List**
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
# **getAssetById**
> AssetResponseDto getAssetById(id, key)
@@ -1154,9 +1212,7 @@ Name | Type | Description | Notes
# **getUserAssetsByDeviceId**
> List getUserAssetsByDeviceId(deviceId)
-
-
-Get all asset of a device that are in the database, ID only.
+Use /asset/device/:deviceId instead - Remove in 1.92 release
### Example
```dart
@@ -1177,7 +1233,7 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
-final deviceId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
+final deviceId = deviceId_example; // String |
try {
final result = api_instance.getUserAssetsByDeviceId(deviceId);
diff --git a/mobile/openapi/doc/CitiesFile.md b/mobile/openapi/doc/CitiesFile.md
deleted file mode 100644
index 9acca959c..000000000
--- a/mobile/openapi/doc/CitiesFile.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# openapi.model.CitiesFile
-
-## Load the model package
-```dart
-import 'package:openapi/api.dart';
-```
-
-## Properties
-Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
-
diff --git a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md
index 36eab4747..9fca6c209 100644
--- a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md
+++ b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md
@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
-**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) | |
**enabled** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 3052d5d8b..894162693 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -83,7 +83,6 @@ part 'model/cq_mode.dart';
part 'model/change_password_dto.dart';
part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart';
-part 'model/cities_file.dart';
part 'model/classification_config.dart';
part 'model/colorspace.dart';
part 'model/create_album_dto.dart';
diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart
index 366c83d57..45c1e1104 100644
--- a/mobile/openapi/lib/api/asset_api.dart
+++ b/mobile/openapi/lib/api/asset_api.dart
@@ -414,6 +414,62 @@ class AssetApi {
return null;
}
+ /// Get all asset of a device that are in the database, ID only.
+ ///
+ /// Note: This method returns the HTTP [Response].
+ ///
+ /// Parameters:
+ ///
+ /// * [String] deviceId (required):
+ Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/asset/device/{deviceId}'
+ .replaceAll('{deviceId}', deviceId);
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Get all asset of a device that are in the database, ID only.
+ ///
+ /// Parameters:
+ ///
+ /// * [String] deviceId (required):
+ Future?> getAllUserAssetsByDeviceId(String deviceId,) async {
+ final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ final responseBody = await _decodeBodyBytes(response);
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
+ .toList();
+
+ }
+ return null;
+ }
+
/// Get a single asset's information
///
/// Note: This method returns the HTTP [Response].
@@ -1211,7 +1267,7 @@ class AssetApi {
return null;
}
- /// Get all asset of a device that are in the database, ID only.
+ /// Use /asset/device/:deviceId instead - Remove in 1.92 release
///
/// Note: This method returns the HTTP [Response].
///
@@ -1244,7 +1300,7 @@ class AssetApi {
);
}
- /// Get all asset of a device that are in the database, ID only.
+ /// Use /asset/device/:deviceId instead - Remove in 1.92 release
///
/// Parameters:
///
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 77a999701..42a0e5cbb 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -255,8 +255,6 @@ class ApiClient {
return CheckExistingAssetsDto.fromJson(value);
case 'CheckExistingAssetsResponseDto':
return CheckExistingAssetsResponseDto.fromJson(value);
- case 'CitiesFile':
- return CitiesFileTypeTransformer().decode(value);
case 'ClassificationConfig':
return ClassificationConfig.fromJson(value);
case 'Colorspace':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index d3f7971e3..728a4ed83 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -73,9 +73,6 @@ String parameterToString(dynamic value) {
if (value is CQMode) {
return CQModeTypeTransformer().encode(value).toString();
}
- if (value is CitiesFile) {
- return CitiesFileTypeTransformer().encode(value).toString();
- }
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
diff --git a/mobile/openapi/lib/model/cities_file.dart b/mobile/openapi/lib/model/cities_file.dart
deleted file mode 100644
index 96f5d8e57..000000000
--- a/mobile/openapi/lib/model/cities_file.dart
+++ /dev/null
@@ -1,91 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-part of openapi.api;
-
-
-class CitiesFile {
- /// Instantiate a new enum with the provided [value].
- const CitiesFile._(this.value);
-
- /// The underlying value of this enum member.
- final String value;
-
- @override
- String toString() => value;
-
- String toJson() => value;
-
- static const cities15000 = CitiesFile._(r'cities15000');
- static const cities5000 = CitiesFile._(r'cities5000');
- static const cities1000 = CitiesFile._(r'cities1000');
- static const cities500 = CitiesFile._(r'cities500');
-
- /// List of all possible values in this [enum][CitiesFile].
- static const values = [
- cities15000,
- cities5000,
- cities1000,
- cities500,
- ];
-
- static CitiesFile? fromJson(dynamic value) => CitiesFileTypeTransformer().decode(value);
-
- static List? listFromJson(dynamic json, {bool growable = false,}) {
- final result = [];
- if (json is List && json.isNotEmpty) {
- for (final row in json) {
- final value = CitiesFile.fromJson(row);
- if (value != null) {
- result.add(value);
- }
- }
- }
- return result.toList(growable: growable);
- }
-}
-
-/// Transformation class that can [encode] an instance of [CitiesFile] to String,
-/// and [decode] dynamic data back to [CitiesFile].
-class CitiesFileTypeTransformer {
- factory CitiesFileTypeTransformer() => _instance ??= const CitiesFileTypeTransformer._();
-
- const CitiesFileTypeTransformer._();
-
- String encode(CitiesFile data) => data.value;
-
- /// Decodes a [dynamic value][data] to a CitiesFile.
- ///
- /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
- /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
- /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
- ///
- /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
- /// and users are still using an old app with the old code.
- CitiesFile? decode(dynamic data, {bool allowNull = true}) {
- if (data != null) {
- switch (data) {
- case r'cities15000': return CitiesFile.cities15000;
- case r'cities5000': return CitiesFile.cities5000;
- case r'cities1000': return CitiesFile.cities1000;
- case r'cities500': return CitiesFile.cities500;
- default:
- if (!allowNull) {
- throw ArgumentError('Unknown enum value to decode: $data');
- }
- }
- }
- return null;
- }
-
- /// Singleton [CitiesFileTypeTransformer] instance.
- static CitiesFileTypeTransformer? _instance;
-}
-
diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart
index 727e5534f..d995d9667 100644
--- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart
+++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart
@@ -13,31 +13,25 @@ part of openapi.api;
class SystemConfigReverseGeocodingDto {
/// Returns a new [SystemConfigReverseGeocodingDto] instance.
SystemConfigReverseGeocodingDto({
- required this.citiesFileOverride,
required this.enabled,
});
- CitiesFile citiesFileOverride;
-
bool enabled;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto &&
- other.citiesFileOverride == citiesFileOverride &&
other.enabled == enabled;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
- (citiesFileOverride.hashCode) +
(enabled.hashCode);
@override
- String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled]';
+ String toString() => 'SystemConfigReverseGeocodingDto[enabled=$enabled]';
Map toJson() {
final json = {};
- json[r'citiesFileOverride'] = this.citiesFileOverride;
json[r'enabled'] = this.enabled;
return json;
}
@@ -50,7 +44,6 @@ class SystemConfigReverseGeocodingDto {
final json = value.cast();
return SystemConfigReverseGeocodingDto(
- citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!,
enabled: mapValueOfType(json, r'enabled')!,
);
}
@@ -99,7 +92,6 @@ class SystemConfigReverseGeocodingDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = {
- 'citiesFileOverride',
'enabled',
};
}
diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart
index 50c35d289..c4f6c8511 100644
--- a/mobile/openapi/test/asset_api_test.dart
+++ b/mobile/openapi/test/asset_api_test.dart
@@ -58,6 +58,13 @@ void main() {
// TODO
});
+ // Get all asset of a device that are in the database, ID only.
+ //
+ //Future> getAllUserAssetsByDeviceId(String deviceId) async
+ test('test getAllUserAssetsByDeviceId', () async {
+ // TODO
+ });
+
// Get a single asset's information
//
//Future getAssetById(String id, { String key }) async
@@ -120,7 +127,7 @@ void main() {
// TODO
});
- // Get all asset of a device that are in the database, ID only.
+ // Use /asset/device/:deviceId instead - Remove in 1.92 release
//
//Future> getUserAssetsByDeviceId(String deviceId) async
test('test getUserAssetsByDeviceId', () async {
diff --git a/mobile/openapi/test/cities_file_test.dart b/mobile/openapi/test/cities_file_test.dart
deleted file mode 100644
index cfe63b754..000000000
--- a/mobile/openapi/test/cities_file_test.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-import 'package:openapi/api.dart';
-import 'package:test/test.dart';
-
-// tests for CitiesFile
-void main() {
-
- group('test CitiesFile', () {
-
- });
-
-}
diff --git a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart
index 12f7655ea..b4aa477df 100644
--- a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart
+++ b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart
@@ -16,11 +16,6 @@ void main() {
// final instance = SystemConfigReverseGeocodingDto();
group('test SystemConfigReverseGeocodingDto', () {
- // CitiesFile citiesFileOverride
- test('to test the property `citiesFileOverride`', () async {
- // TODO
- });
-
// bool enabled
test('to test the property `enabled`', () async {
// TODO
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index a573f6bf7..7cb4188d9 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
+ analyzer_plugin:
+ dependency: transitive
+ description:
+ name: analyzer_plugin
+ sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.2"
archive:
dependency: transitive
description:
@@ -201,6 +209,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.7.0"
+ ci:
+ dependency: transitive
+ description:
+ name: ci
+ sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.0"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.0"
clock:
dependency: transitive
description:
@@ -281,6 +305,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
+ custom_lint:
+ dependency: "direct dev"
+ description:
+ name: custom_lint
+ sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.6"
+ custom_lint_builder:
+ dependency: transitive
+ description:
+ name: custom_lint_builder
+ sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.6"
+ custom_lint_core:
+ dependency: transitive
+ description:
+ name: custom_lint_core
+ sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.6"
dart_style:
dependency: transitive
description:
@@ -455,10 +503,10 @@ packages:
dependency: "direct main"
description:
name: flutter_hooks
- sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c"
+ sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec"
url: "https://pub.dev"
source: hosted
- version: "0.18.6"
+ version: "0.20.3"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -540,10 +588,10 @@ packages:
dependency: transitive
description:
name: flutter_riverpod
- sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3
+ sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe"
url: "https://pub.dev"
source: hosted
- version: "2.3.7"
+ version: "2.4.5"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -578,6 +626,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.2"
+ freezed_annotation:
+ dependency: transitive
+ description:
+ name: freezed_annotation
+ sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
frontend_server_client:
dependency: transitive
description:
@@ -659,10 +715,18 @@ packages:
dependency: "direct main"
description:
name: hooks_riverpod
- sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18"
+ sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73"
url: "https://pub.dev"
source: hosted
- version: "2.3.8"
+ version: "2.4.5"
+ hotreloader:
+ dependency: transitive
+ description:
+ name: hotreloader
+ sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
html:
dependency: transitive
description:
@@ -1175,10 +1239,42 @@ packages:
dependency: transitive
description:
name: riverpod
- sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793
+ sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55"
url: "https://pub.dev"
source: hosted
- version: "2.3.7"
+ version: "2.4.5"
+ riverpod_analyzer_utils:
+ dependency: transitive
+ description:
+ name: riverpod_analyzer_utils
+ sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4"
+ riverpod_annotation:
+ dependency: "direct main"
+ description:
+ name: riverpod_annotation
+ sha256: "9330309e4400f40e39a2a1d1c340e775d0fd23451cf2dd2286e03c7896fd2bd5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ riverpod_generator:
+ dependency: "direct dev"
+ description:
+ name: riverpod_generator
+ sha256: "5b36ad2f2b562cffb37212e8d59390b25499bf045b732276e30a207b16a25f61"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.3"
+ riverpod_lint:
+ dependency: "direct dev"
+ description:
+ name: riverpod_lint
+ sha256: "70198738c3047ae4f6517ef1a2011a8514a980a52576c7f629a3a08810319a02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
rxdart:
dependency: transitive
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index d5c2a8571..d2f8a9d2d 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
-version: 1.87.0+111
+version: 1.89.0+113
isar_version: &isar_version 3.1.0+1
environment:
@@ -14,8 +14,9 @@ dependencies:
path_provider_ios:
photo_manager: ^2.7.2
- flutter_hooks: ^0.18.6
- hooks_riverpod: ^2.2.0
+ flutter_hooks: ^0.20.3
+ hooks_riverpod: ^2.4.0
+ riverpod_annotation: ^2.3.0
cached_network_image: ^3.2.2
flutter_cache_manager: ^3.3.0
intl: ^0.18.0
@@ -86,6 +87,9 @@ dev_dependencies:
mockito: ^5.3.2
integration_test:
sdk: flutter
+ custom_lint: ^0.5.6
+ riverpod_lint: ^2.1.0
+ riverpod_generator: ^2.3.3
flutter:
uses-material-design: true
@@ -93,27 +97,28 @@ flutter:
- assets/
- assets/i18n/
fonts:
- - family: WorkSans
- fonts:
- - asset: fonts/WorkSans.ttf
- - asset: fonts/WorkSans-Italic.ttf
- style: italic
- # - asset: fonts/WorkSans-Medium.ttf
- # weight: 500
- # - asset: fonts/WorkSans-SemiBold.ttf
- # weight: 600
- # - asset: fonts/WorkSans-Bold.ttf
- # weight: 700
- # - asset: fonts/WorkSans-ExtraBold.ttf
- # weight: 800
- # - asset: fonts/WorkSans-Black.ttf
- # weight: 900
- family: SnowburstOne
fonts:
- asset: fonts/SnowburstOne.ttf
- family: Inconsolata
fonts:
- asset: fonts/Inconsolata-Regular.ttf
+ - family: Overpass
+ fonts:
+ - asset: fonts/overpass/Overpass-Regular.ttf
+ weight: 400
+ - asset: fonts/overpass/Overpass-Italic.ttf
+ style: italic
+ - asset: fonts/overpass/Overpass-Medium.ttf
+ weight: 500
+ - asset: fonts/overpass/Overpass-SemiBold.ttf
+ weight: 600
+ - asset: fonts/overpass/Overpass-Bold.ttf
+ weight: 700
+ - family: OverpassMono
+ fonts:
+ - asset: fonts/overpass/OverpassMono.ttf
+
flutter_icons:
image_path_android: "assets/immich-logo-no-outline.png"
image_path_ios: "assets/immich-logo-no-outline.png"
diff --git a/renovate.json b/renovate.json
index 551316f27..12124c359 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,26 +1,64 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": ["config:base"],
+ "extends": ["config:base", "docker:pinDigests"],
+ "minimumReleaseAge": "5 days",
"packageRules": [
+ {
+ "matchFileNames": ["cli/**"],
+ "groupName": "@immich/cli",
+ "matchUpdateTypes": ["minor", "patch"],
+ "schedule": "on tuesday"
+ },
+ {
+ "matchFileNames": ["docs/**"],
+ "groupName": "docs",
+ "matchUpdateTypes": ["minor", "patch"],
+ "schedule": "on tuesday"
+ },
{
"matchFileNames": ["mobile/**"],
"groupName": "mobile",
- "matchUpdateTypes": ["minor", "patch"]
+ "matchUpdateTypes": ["minor", "patch"],
+ "schedule": "on tuesday"
},
{
"matchFileNames": ["server/**"],
"groupName": "server",
"matchUpdateTypes": ["minor", "patch"],
- "excludePackagePrefixes": ["exiftool"]
+ "excludePackagePrefixes": ["exiftool"],
+ "schedule": "on tuesday"
},
{
"groupName": "exiftool",
- "matchPackagePrefixes": ["exiftool"]
+ "matchPackagePrefixes": ["exiftool"],
+ "schedule": "on tuesday"
},
{
"matchFileNames": ["web/**"],
"groupName": "web",
- "matchUpdateTypes": ["minor", "patch"]
+ "matchUpdateTypes": ["minor", "patch"],
+ "schedule": "on tuesday"
+ },
+ {
+ "matchFileNames": ["machine-learning/**"],
+ "groupName": "machine-learning",
+ "matchUpdateTypes": ["minor", "patch"],
+ "schedule": "on tuesday"
+ },
+ {
+ "matchFileNames": [".github/**"],
+ "groupName": "github-actions",
+ "schedule": "on tuesday"
+ },
+ {
+ "groupName": "base-image",
+ "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"]
+ },
+ {
+ "matchDatasources": ["docker"],
+ "matchPackageNames": ["node"],
+ "versionCompatibility": "^(?[^-]+)(?-.*)?$",
+ "versioning": "node"
}
],
"ignoreDeps": [
@@ -29,5 +67,5 @@
"vector_map_tiles",
"flutter_map",
"flutter_map_heatmap"
- ],
+ ]
}
diff --git a/server/.eslintrc.js b/server/.eslintrc.js
index 17a0a2dd6..2e46281fe 100644
--- a/server/.eslintrc.js
+++ b/server/.eslintrc.js
@@ -19,6 +19,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
+ curly: 2,
'prettier/prettier': 0,
},
};
diff --git a/server/Dockerfile b/server/Dockerfile
index c780a116e..bd269a378 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,5 +1,5 @@
# dev build
-FROM ghcr.io/immich-app/base-server-dev:20231109 as dev
+FROM ghcr.io/immich-app/base-server-dev:20231125@sha256:f33b6eaf384e76ef3705a6e2cc76d276144ad6d3366b82f9b45b07d6a19285e2 as dev
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
@@ -13,7 +13,7 @@ RUN npm run build
RUN npm prune --omit=dev --omit=optional
# web build
-FROM node:20.9-alpine3.18 as web
+FROM node:iron-alpine3.18 as web
WORKDIR /usr/src/app
COPY web/package.json web/package-lock.json ./
@@ -23,7 +23,7 @@ RUN npm run build
# prod build
-FROM ghcr.io/immich-app/base-server-prod:20231109
+FROM ghcr.io/immich-app/base-server-prod:20231125@sha256:a0e15f5bf87a97a79a399a5adffb5fe5befc18fb212e8341e744d958fe41e32a
WORKDIR /usr/src/app
ENV NODE_ENV=production
@@ -31,7 +31,7 @@ COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin
COPY --from=web /usr/src/app/build ./www
-COPY server/assets assets
+COPY server/resources resources
COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./
RUN npm link && npm cache clean --force
diff --git a/server/bin/cli.sh b/server/bin/cli.sh
index 148954d9a..3c6b1512e 100755
--- a/server/bin/cli.sh
+++ b/server/bin/cli.sh
@@ -1,2 +1,2 @@
#!/usr/bin/env bash
-node ./node_modules/immich/bin/index "$@"
+npx immich "$@"
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index 42fc81dd9..66ebe1920 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -1219,6 +1219,51 @@
]
}
},
+ "/asset/device/{deviceId}": {
+ "get": {
+ "description": "Get all asset of a device that are in the database, ID only.",
+ "operationId": "getAllUserAssetsByDeviceId",
+ "parameters": [
+ {
+ "name": "deviceId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Asset"
+ ]
+ }
+ },
"/asset/download/archive": {
"post": {
"operationId": "downloadArchive",
@@ -2281,7 +2326,7 @@
},
"/asset/{deviceId}": {
"get": {
- "description": "Get all asset of a device that are in the database, ID only.",
+ "deprecated": true,
"operationId": "getUserAssetsByDeviceId",
"parameters": [
{
@@ -2289,7 +2334,6 @@
"required": true,
"in": "path",
"schema": {
- "format": "uuid",
"type": "string"
}
}
@@ -2320,6 +2364,7 @@
"api_key": []
}
],
+ "summary": "Use /asset/device/:deviceId instead - Remove in 1.92 release",
"tags": [
"Asset"
]
@@ -6052,7 +6097,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
- "version": "1.87.0",
+ "version": "1.89.0",
"contact": {}
},
"tags": [],
@@ -6944,15 +6989,6 @@
],
"type": "object"
},
- "CitiesFile": {
- "enum": [
- "cities15000",
- "cities5000",
- "cities1000",
- "cities500"
- ],
- "type": "string"
- },
"ClassificationConfig": {
"properties": {
"enabled": {
@@ -9067,15 +9103,11 @@
},
"SystemConfigReverseGeocodingDto": {
"properties": {
- "citiesFileOverride": {
- "$ref": "#/components/schemas/CitiesFile"
- },
"enabled": {
"type": "boolean"
}
},
"required": [
- "citiesFileOverride",
"enabled"
],
"type": "object"
diff --git a/server/package-lock.json b/server/package-lock.json
index 3213b2aae..19a4851c8 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,15 +1,16 @@
{
"name": "immich",
- "version": "1.87.0",
+ "version": "1.89.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
- "version": "1.87.0",
+ "version": "1.89.0",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",
+ "@immich/cli": "^2.0.3",
"@nestjs/bullmq": "^10.0.1",
"@nestjs/common": "^10.2.2",
"@nestjs/config": "^3.0.0",
@@ -28,17 +29,15 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
- "exiftool-vendored": "~23.1.0",
- "exiftool-vendored.pl": "12.67",
+ "exiftool-vendored": "~23.5.0",
+ "exiftool-vendored.pl": "12.70",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^7.0.7",
"glob": "^10.3.3",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
- "immich": "^0.41.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
- "local-reverse-geocoder": "0.16.5",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
"mv": "^2.1.1",
@@ -64,14 +63,14 @@
"@nestjs/testing": "^10.2.2",
"@openapitools/openapi-generator-cli": "2.7.0",
"@testcontainers/postgresql": "^10.2.1",
- "@types/archiver": "^5.3.2",
+ "@types/archiver": "^6.0.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
- "@types/jest": "29.5.8",
+ "@types/jest": "29.5.10",
"@types/jest-when": "^3.5.2",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
@@ -805,9 +804,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
+ "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1053,6 +1052,31 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
+ "node_modules/@immich/cli": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.4.tgz",
+ "integrity": "sha512-Fzi4sESi1pD0YdqhDAlGgR8WfeBEsfxuNpbzgY6DklmgTeIDOh29pv+ESo080uWfKhXdBBhYgc1ZUc6m5krxQw==",
+ "dependencies": {
+ "axios": "^1.6.2",
+ "byte-size": "^8.1.1",
+ "cli-progress": "^3.12.0",
+ "commander": "^11.0.0",
+ "form-data": "^4.0.0",
+ "glob": "^10.3.1",
+ "yaml": "^2.3.1"
+ },
+ "bin": {
+ "immich": "dist/src/index.js"
+ }
+ },
+ "node_modules/@immich/cli/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@@ -1827,10 +1851,23 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@nestjs/cli/node_modules/typescript": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/@nestjs/common": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.9.tgz",
- "integrity": "sha512-i7vb2zMLJUDIPqjfBhMkgIITK1AnKDkFYSsM+aaRHpNa9xv/CwsiQuINaXfzStMpnwjkq5FDE3aoF0wkTfD2cQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.10.tgz",
+ "integrity": "sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.6.2",
@@ -1876,9 +1913,9 @@
}
},
"node_modules/@nestjs/core": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.9.tgz",
- "integrity": "sha512-Hl6HC9hR7JD3YmzwcveBKeydaq9cguEsMdEghzLuVH3VEH0M+bTFHjCIKhsxMez4/O7/K6n3EhNx1Et4Z+BqWg==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.10.tgz",
+ "integrity": "sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/opencollective": "0.3.2",
@@ -1937,9 +1974,9 @@
}
},
"node_modules/@nestjs/platform-express": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.9.tgz",
- "integrity": "sha512-r6BSMJmLLeNgyPZJ9F8wQWCXH6rrMHMd9QbCfvyUmETci5Ofy6atiYVVXl7Ms1rAi2EEnXpVCuoydHBBqSlTbg==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.10.tgz",
+ "integrity": "sha512-U4KDgtMjH8TqEvt0RzC/POP8ABvL9bYoCScvlGtFSKgVGaMLBKkZ4+jHtbQx6qItYSlBBRUuz/dveMZCObfrkQ==",
"dependencies": {
"body-parser": "1.20.2",
"cors": "2.8.5",
@@ -1962,9 +1999,9 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@nestjs/platform-socket.io": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.9.tgz",
- "integrity": "sha512-xuXsUWUtgzdRnNBSWZADQv0sShBOsyHK7iEwwIpFMerqbthSMwjsyUv0s3hDoPEnS6Nf4MMf2KReD5JBAnMBFQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.10.tgz",
+ "integrity": "sha512-JBuemeIBp2mpp+z7D12oa22k83TnDTxyQDMKZpO/B2/QnBVR+2C4EZ07/XOct14LQXO6vIjmT0iYYCZbNvczjw==",
"dependencies": {
"socket.io": "4.7.2",
"tslib": "2.6.2"
@@ -2058,9 +2095,9 @@
}
},
"node_modules/@nestjs/testing": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.9.tgz",
- "integrity": "sha512-E+66R27Op+WAQHHH6RnUsz7QpKApl4Bn42nheCAGvS/sxbaDJ8RKtm4stE4Iz2aioPCUvRi8j4z8Ze73k0CcGQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz",
+ "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==",
"dev": true,
"dependencies": {
"tslib": "2.6.2"
@@ -2118,9 +2155,9 @@
}
},
"node_modules/@nestjs/websockets": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.9.tgz",
- "integrity": "sha512-Hp/ioNMcBtCzkubgcDHeA5KH4YLBL1jHfMEacLctKDa20Kn/LFXhJhXVWEr4jEzUKXbD+Q+GyYk1V/1rJi6eHA==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.10.tgz",
+ "integrity": "sha512-L1AkxwLUj/ntk26jO1SXYl3GRElQE6Fikzfy/3MPFURk0GDs7tHUzLcb8BC8q8u5ZpUjBAC2wFVQzrY5R0MHNw==",
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
@@ -2692,12 +2729,12 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"node_modules/@testcontainers/postgresql": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.2.tgz",
- "integrity": "sha512-G1xJKe8omeNzngK0dj4R2cSYxWyOUdTXD/oBA03AqIwdReq/gi4WjT6CJqGbkqQy9opXZV6ug3gHMja+wM5BCA==",
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz",
+ "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==",
"dev": true,
"dependencies": {
- "testcontainers": "^10.2.2"
+ "testcontainers": "^10.3.2"
}
},
"node_modules/@tsconfig/node10": {
@@ -2756,9 +2793,9 @@
}
},
"node_modules/@types/archiver": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.4.tgz",
- "integrity": "sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
+ "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
"dev": true,
"dependencies": {
"@types/readdir-glob": "*"
@@ -2871,6 +2908,26 @@
"cron": "*"
}
},
+ "node_modules/@types/docker-modem": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
+ "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/ssh2": "*"
+ }
+ },
+ "node_modules/@types/dockerode": {
+ "version": "3.3.23",
+ "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz",
+ "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==",
+ "dev": true,
+ "dependencies": {
+ "@types/docker-modem": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.44.3",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz",
@@ -2946,9 +3003,9 @@
"dev": true
},
"node_modules/@types/imagemin": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.4.tgz",
- "integrity": "sha512-t7vady38h/FTQAxFe6gJvaTxjgi/uw54ZrDbqyKx3yOMPu3NQjQexCoLxBR03FRv0HcKJMV2MqGLeY7BuPl6/A==",
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
+ "integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -2989,9 +3046,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.8",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
- "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
+ "version": "29.5.10",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz",
+ "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -3014,15 +3071,15 @@
"dev": true
},
"node_modules/@types/lodash": {
- "version": "4.14.201",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
- "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==",
+ "version": "4.14.202",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
+ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"node_modules/@types/luxon": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
- "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ=="
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz",
+ "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA=="
},
"node_modules/@types/mime": {
"version": "1.3.3",
@@ -3040,9 +3097,9 @@
}
},
"node_modules/@types/multer": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.10.tgz",
- "integrity": "sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==",
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
+ "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==",
"dev": true,
"dependencies": {
"@types/express": "*"
@@ -3055,9 +3112,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
- "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
+ "version": "20.10.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
+ "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3084,9 +3141,9 @@
}
},
"node_modules/@types/semver": {
- "version": "7.5.5",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
- "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==",
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"node_modules/@types/send": {
@@ -3199,16 +3256,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz",
- "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+ "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/type-utils": "6.11.0",
- "@typescript-eslint/utils": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/type-utils": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -3234,15 +3291,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz",
- "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+ "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/typescript-estree": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4"
},
"engines": {
@@ -3262,13 +3319,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz",
- "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0"
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3279,13 +3336,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz",
- "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+ "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "6.11.0",
- "@typescript-eslint/utils": "6.11.0",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -3306,9 +3363,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz",
- "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3319,13 +3376,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz",
- "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -3346,17 +3403,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz",
- "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/typescript-estree": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
"semver": "^7.5.4"
},
"engines": {
@@ -3371,12 +3428,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz",
- "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.11.0",
+ "@typescript-eslint/types": "6.13.1",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -4094,18 +4151,6 @@
"node": ">=0.6"
}
},
- "node_modules/binary": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
- "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
- "dependencies": {
- "buffers": "~0.1.1",
- "chainsaw": "~0.1.0"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -4291,14 +4336,6 @@
"node": ">=4"
}
},
- "node_modules/buffers": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
- "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
- "engines": {
- "node": ">=0.2.0"
- }
- },
"node_modules/buildcheck": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
@@ -4310,9 +4347,9 @@
}
},
"node_modules/bullmq": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.0.tgz",
- "integrity": "sha512-2IjTzXfTkXQ+WNRy2/CVupnHJtqp6JpxacIvYbru2EvporUALnIcpiSpjJbk4V6kAbsYvrV2wRdUKllb+LfssQ==",
+ "version": "4.14.4",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz",
+ "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==",
"dependencies": {
"cron-parser": "^4.6.0",
"glob": "^8.0.3",
@@ -4397,6 +4434,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/byte-size": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
+ "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==",
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -4454,17 +4499,6 @@
}
]
},
- "node_modules/chainsaw": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
- "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
- "dependencies": {
- "traverse": ">=0.3.0 <0.4"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5115,19 +5149,6 @@
"node": ">= 8"
}
},
- "node_modules/csv-parse": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz",
- "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw=="
- },
- "node_modules/data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -5495,15 +5516,6 @@
"node": ">= 6.0.0"
}
},
- "node_modules/docker-compose/node_modules/yaml": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
- "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
- "dev": true,
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/docker-modem": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz",
@@ -6013,43 +6025,34 @@
"dev": true
},
"node_modules/exiftool-vendored": {
- "version": "23.1.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.1.0.tgz",
- "integrity": "sha512-sZ1OUpvAWbUCCoidMMKDTTJ3hHE3mHxb4ihWKmta/eQYYMR54Mssp6+Nf7HoFvY//nX5YK2VCOGVexGGuhM8Bw==",
+ "version": "23.5.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.5.0.tgz",
+ "integrity": "sha512-6wlCNFFOcOcRa8GpuCzg0dhOcRMipONMZidP3jYUF0g7YkhknKxG4aA0BdmeH2Tp3Hm0286egicslGW9d7fZoA==",
"dependencies": {
"@photostructure/tz-lookup": "^8.0.0",
- "@types/luxon": "^3.3.2",
+ "@types/luxon": "^3.3.5",
"batch-cluster": "^12.1.0",
"he": "^1.2.0",
- "luxon": "^3.4.3"
+ "luxon": "^3.4.4"
},
"optionalDependencies": {
- "exiftool-vendored.exe": "12.67.0",
- "exiftool-vendored.pl": "12.67.0"
+ "exiftool-vendored.exe": "12.70.0",
+ "exiftool-vendored.pl": "12.70.0"
}
},
"node_modules/exiftool-vendored.exe": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.67.0.tgz",
- "integrity": "sha512-wzgMDoL/VWH34l38g22cVUn43mVFtTSVj0HRjfjR46+4fGwpSvSueeYbwLCZ5NvBAVINCS5Rz9Rl2DVmqoIjsw==",
+ "version": "12.70.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.70.0.tgz",
+ "integrity": "sha512-Jp78fvWCls2q3+6P0GxEpgBEgIj2MJsrOKxfAnVzADO3BZ8jRTdYCHVMUoxbiuzIcqzLmQA2mPLjhQ35Y4MyeA==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz",
- "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==",
- "os": [
- "!win32"
- ]
- },
- "node_modules/exiftool-vendored/node_modules/exiftool-vendored.pl": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz",
- "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==",
- "optional": true,
+ "version": "12.70.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.70.0.tgz",
+ "integrity": "sha512-nozVKVE7Leq2lQ+Kd6VbX+S04z0TKYGd42F1odwEl8AtWNlJViZ7rsi+OLHkJ0llWmObotviYAsj/xhF28qPSQ==",
"os": [
"!win32"
]
@@ -6272,41 +6275,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fdir": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz",
- "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==",
- "peerDependencies": {
- "picomatch": "2.x"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "dependencies": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- },
- "engines": {
- "node": "^12.20 || >= 14.13"
- }
- },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -6560,17 +6528,6 @@
"node": ">= 6"
}
},
- "node_modules/formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "dependencies": {
- "fetch-blob": "^3.1.2"
- },
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/formidable": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
@@ -7107,105 +7064,6 @@
"node": ">= 4"
}
},
- "node_modules/immich": {
- "version": "0.41.0",
- "resolved": "https://registry.npmjs.org/immich/-/immich-0.41.0.tgz",
- "integrity": "sha512-F+DIJ41/1L4Cqs0NqubcD2qttYcxTs32DCZS2l6U7d+a4SVtbxYwtEkvljSKACka1vokmHetpveGyRau2FdZ8w==",
- "dependencies": {
- "axios": "^0.26.0",
- "chalk": "^2.4.1",
- "cli-progress": "^3.10.0",
- "commander": "^9.0.0",
- "fdir": "^5.2.0",
- "form-data": "^4.0.0",
- "mime-types": "^2.1.34",
- "p-limit": "3.1.0",
- "systeminformation": "^5.11.6"
- },
- "bin": {
- "immich": "bin/index.js"
- }
- },
- "node_modules/immich/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/immich/node_modules/axios": {
- "version": "0.26.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
- "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
- "dependencies": {
- "follow-redirects": "^1.14.8"
- }
- },
- "node_modules/immich/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/immich/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/immich/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
- },
- "node_modules/immich/node_modules/commander": {
- "version": "9.5.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
- "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
- "engines": {
- "node": "^12.20.0 || >=14"
- }
- },
- "node_modules/immich/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/immich/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/immich/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -8407,11 +8265,6 @@
"graceful-fs": "^4.1.6"
}
},
- "node_modules/kdt": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz",
- "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg=="
- },
"node_modules/keyv": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
@@ -8509,41 +8362,6 @@
"node": ">=6.11.5"
}
},
- "node_modules/local-reverse-geocoder": {
- "version": "0.16.5",
- "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz",
- "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==",
- "hasInstallScript": true,
- "dependencies": {
- "async": "^3.2.4",
- "csv-parse": "^5.5.0",
- "debug": "^4.3.4",
- "kdt": "^0.1.0",
- "node-fetch": "^3.3.2",
- "unzip-stream": "^0.3.1"
- },
- "engines": {
- "node": ">=11.0.0",
- "npm": ">=6.4.1"
- }
- },
- "node_modules/local-reverse-geocoder/node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
- }
- },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -9161,24 +8979,6 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "engines": {
- "node": ">=10.5.0"
- }
- },
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@@ -9468,6 +9268,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -9742,7 +9543,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "devOptional": true,
+ "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -11289,31 +11090,6 @@
"url": "https://opencollective.com/unts"
}
},
- "node_modules/systeminformation": {
- "version": "5.21.9",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz",
- "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==",
- "os": [
- "darwin",
- "linux",
- "win32",
- "freebsd",
- "openbsd",
- "netbsd",
- "sunos",
- "android"
- ],
- "bin": {
- "systeminformation": "lib/cli.js"
- },
- "engines": {
- "node": ">=8.0.0"
- },
- "funding": {
- "type": "Buy me a coffee",
- "url": "https://www.buymeacoffee.com/systeminfo"
- }
- },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -11505,12 +11281,13 @@
}
},
"node_modules/testcontainers": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.2.tgz",
- "integrity": "sha512-5GZ93rtoVXMx/s3xZjydftrKLnv1Yf+ETzGkXYRCm16LB60W48SGodxuiouYvNlVy0y0ogoQhdOw3DqsPActEA==",
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz",
+ "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==",
"dev": true,
"dependencies": {
"@balena/dockerignore": "^1.0.2",
+ "@types/dockerode": "^3.3.21",
"archiver": "^5.3.2",
"async-lock": "^1.4.0",
"byline": "^5.0.0",
@@ -11825,14 +11602,6 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
- "node_modules/traverse": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
- "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -12303,9 +12072,9 @@
}
},
"node_modules/typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
@@ -12422,15 +12191,6 @@
"node": ">=8"
}
},
- "node_modules/unzip-stream": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz",
- "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==",
- "dependencies": {
- "binary": "^0.3.0",
- "mkdirp": "^0.5.1"
- }
- },
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -12588,14 +12348,6 @@
"defaults": "^1.0.3"
}
},
- "node_modules/web-streams-polyfill": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
- "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -12888,6 +12640,14 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
+ "node_modules/yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -12934,6 +12694,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -13472,9 +13233,9 @@
}
},
"@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
+ "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==",
"requires": {
"regenerator-runtime": "^0.14.0"
}
@@ -13669,6 +13430,27 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
+ "@immich/cli": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.4.tgz",
+ "integrity": "sha512-Fzi4sESi1pD0YdqhDAlGgR8WfeBEsfxuNpbzgY6DklmgTeIDOh29pv+ESo080uWfKhXdBBhYgc1ZUc6m5krxQw==",
+ "requires": {
+ "axios": "^1.6.2",
+ "byte-size": "^8.1.1",
+ "cli-progress": "^3.12.0",
+ "commander": "^11.0.0",
+ "form-data": "^4.0.0",
+ "glob": "^10.3.1",
+ "yaml": "^2.3.1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="
+ }
+ }
+ },
"@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@@ -14244,13 +14026,19 @@
}
}
}
+ },
+ "typescript": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "dev": true
}
}
},
"@nestjs/common": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.9.tgz",
- "integrity": "sha512-i7vb2zMLJUDIPqjfBhMkgIITK1AnKDkFYSsM+aaRHpNa9xv/CwsiQuINaXfzStMpnwjkq5FDE3aoF0wkTfD2cQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.10.tgz",
+ "integrity": "sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==",
"requires": {
"iterare": "1.2.1",
"tslib": "2.6.2",
@@ -14276,9 +14064,9 @@
}
},
"@nestjs/core": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.9.tgz",
- "integrity": "sha512-Hl6HC9hR7JD3YmzwcveBKeydaq9cguEsMdEghzLuVH3VEH0M+bTFHjCIKhsxMez4/O7/K6n3EhNx1Et4Z+BqWg==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.10.tgz",
+ "integrity": "sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==",
"requires": {
"@nuxtjs/opencollective": "0.3.2",
"fast-safe-stringify": "2.1.1",
@@ -14302,9 +14090,9 @@
"requires": {}
},
"@nestjs/platform-express": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.9.tgz",
- "integrity": "sha512-r6BSMJmLLeNgyPZJ9F8wQWCXH6rrMHMd9QbCfvyUmETci5Ofy6atiYVVXl7Ms1rAi2EEnXpVCuoydHBBqSlTbg==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.10.tgz",
+ "integrity": "sha512-U4KDgtMjH8TqEvt0RzC/POP8ABvL9bYoCScvlGtFSKgVGaMLBKkZ4+jHtbQx6qItYSlBBRUuz/dveMZCObfrkQ==",
"requires": {
"body-parser": "1.20.2",
"cors": "2.8.5",
@@ -14321,9 +14109,9 @@
}
},
"@nestjs/platform-socket.io": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.9.tgz",
- "integrity": "sha512-xuXsUWUtgzdRnNBSWZADQv0sShBOsyHK7iEwwIpFMerqbthSMwjsyUv0s3hDoPEnS6Nf4MMf2KReD5JBAnMBFQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.10.tgz",
+ "integrity": "sha512-JBuemeIBp2mpp+z7D12oa22k83TnDTxyQDMKZpO/B2/QnBVR+2C4EZ07/XOct14LQXO6vIjmT0iYYCZbNvczjw==",
"requires": {
"socket.io": "4.7.2",
"tslib": "2.6.2"
@@ -14378,9 +14166,9 @@
}
},
"@nestjs/testing": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.9.tgz",
- "integrity": "sha512-E+66R27Op+WAQHHH6RnUsz7QpKApl4Bn42nheCAGvS/sxbaDJ8RKtm4stE4Iz2aioPCUvRi8j4z8Ze73k0CcGQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz",
+ "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==",
"dev": true,
"requires": {
"tslib": "2.6.2"
@@ -14410,9 +14198,9 @@
}
},
"@nestjs/websockets": {
- "version": "10.2.9",
- "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.9.tgz",
- "integrity": "sha512-Hp/ioNMcBtCzkubgcDHeA5KH4YLBL1jHfMEacLctKDa20Kn/LFXhJhXVWEr4jEzUKXbD+Q+GyYk1V/1rJi6eHA==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.10.tgz",
+ "integrity": "sha512-L1AkxwLUj/ntk26jO1SXYl3GRElQE6Fikzfy/3MPFURk0GDs7tHUzLcb8BC8q8u5ZpUjBAC2wFVQzrY5R0MHNw==",
"requires": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
@@ -14836,12 +14624,12 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"@testcontainers/postgresql": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.2.tgz",
- "integrity": "sha512-G1xJKe8omeNzngK0dj4R2cSYxWyOUdTXD/oBA03AqIwdReq/gi4WjT6CJqGbkqQy9opXZV6ug3gHMja+wM5BCA==",
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz",
+ "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==",
"dev": true,
"requires": {
- "testcontainers": "^10.2.2"
+ "testcontainers": "^10.3.2"
}
},
"@tsconfig/node10": {
@@ -14891,9 +14679,9 @@
}
},
"@types/archiver": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.4.tgz",
- "integrity": "sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
+ "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
"dev": true,
"requires": {
"@types/readdir-glob": "*"
@@ -15005,6 +14793,26 @@
"cron": "*"
}
},
+ "@types/docker-modem": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
+ "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/ssh2": "*"
+ }
+ },
+ "@types/dockerode": {
+ "version": "3.3.23",
+ "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz",
+ "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==",
+ "dev": true,
+ "requires": {
+ "@types/docker-modem": "*",
+ "@types/node": "*"
+ }
+ },
"@types/eslint": {
"version": "8.44.3",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz",
@@ -15080,9 +14888,9 @@
"dev": true
},
"@types/imagemin": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.4.tgz",
- "integrity": "sha512-t7vady38h/FTQAxFe6gJvaTxjgi/uw54ZrDbqyKx3yOMPu3NQjQexCoLxBR03FRv0HcKJMV2MqGLeY7BuPl6/A==",
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
+ "integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -15123,9 +14931,9 @@
}
},
"@types/jest": {
- "version": "29.5.8",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
- "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
+ "version": "29.5.10",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz",
+ "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==",
"dev": true,
"requires": {
"expect": "^29.0.0",
@@ -15148,15 +14956,15 @@
"dev": true
},
"@types/lodash": {
- "version": "4.14.201",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
- "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==",
+ "version": "4.14.202",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
+ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"@types/luxon": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
- "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ=="
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz",
+ "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA=="
},
"@types/mime": {
"version": "1.3.3",
@@ -15174,9 +14982,9 @@
}
},
"@types/multer": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.10.tgz",
- "integrity": "sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==",
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
+ "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==",
"dev": true,
"requires": {
"@types/express": "*"
@@ -15189,9 +14997,9 @@
"dev": true
},
"@types/node": {
- "version": "20.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
- "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
+ "version": "20.10.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
+ "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -15218,9 +15026,9 @@
}
},
"@types/semver": {
- "version": "7.5.5",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
- "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==",
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"@types/send": {
@@ -15333,16 +15141,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz",
- "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+ "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/type-utils": "6.11.0",
- "@typescript-eslint/utils": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/type-utils": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -15352,54 +15160,54 @@
}
},
"@typescript-eslint/parser": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz",
- "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+ "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/typescript-estree": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz",
- "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+ "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0"
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1"
}
},
"@typescript-eslint/type-utils": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz",
- "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+ "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "6.11.0",
- "@typescript-eslint/utils": "6.11.0",
+ "@typescript-eslint/typescript-estree": "6.13.1",
+ "@typescript-eslint/utils": "6.13.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
}
},
"@typescript-eslint/types": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz",
- "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+ "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz",
- "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+ "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/visitor-keys": "6.11.0",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/visitor-keys": "6.13.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -15408,27 +15216,27 @@
}
},
"@typescript-eslint/utils": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz",
- "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+ "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.11.0",
- "@typescript-eslint/types": "6.11.0",
- "@typescript-eslint/typescript-estree": "6.11.0",
+ "@typescript-eslint/scope-manager": "6.13.1",
+ "@typescript-eslint/types": "6.13.1",
+ "@typescript-eslint/typescript-estree": "6.13.1",
"semver": "^7.5.4"
}
},
"@typescript-eslint/visitor-keys": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz",
- "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==",
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+ "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "6.11.0",
+ "@typescript-eslint/types": "6.13.1",
"eslint-visitor-keys": "^3.4.1"
}
},
@@ -16009,15 +15817,6 @@
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
"dev": true
},
- "binary": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
- "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
- "requires": {
- "buffers": "~0.1.1",
- "chainsaw": "~0.1.0"
- }
- },
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -16149,11 +15948,6 @@
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
- "buffers": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
- "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
- },
"buildcheck": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
@@ -16162,9 +15956,9 @@
"optional": true
},
"bullmq": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.0.tgz",
- "integrity": "sha512-2IjTzXfTkXQ+WNRy2/CVupnHJtqp6JpxacIvYbru2EvporUALnIcpiSpjJbk4V6kAbsYvrV2wRdUKllb+LfssQ==",
+ "version": "4.14.4",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz",
+ "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==",
"requires": {
"cron-parser": "^4.6.0",
"glob": "^8.0.3",
@@ -16230,6 +16024,11 @@
"integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
"dev": true
},
+ "byte-size": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
+ "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg=="
+ },
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -16261,14 +16060,6 @@
"integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==",
"dev": true
},
- "chainsaw": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
- "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
- "requires": {
- "traverse": ">=0.3.0 <0.4"
- }
- },
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -16748,16 +16539,6 @@
"which": "^2.0.1"
}
},
- "csv-parse": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz",
- "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw=="
- },
- "data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
- },
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -16994,14 +16775,6 @@
"dev": true,
"requires": {
"yaml": "^2.2.2"
- },
- "dependencies": {
- "yaml": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
- "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
- "dev": true
- }
}
},
"docker-modem": {
@@ -17382,37 +17155,29 @@
}
},
"exiftool-vendored": {
- "version": "23.1.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.1.0.tgz",
- "integrity": "sha512-sZ1OUpvAWbUCCoidMMKDTTJ3hHE3mHxb4ihWKmta/eQYYMR54Mssp6+Nf7HoFvY//nX5YK2VCOGVexGGuhM8Bw==",
+ "version": "23.5.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.5.0.tgz",
+ "integrity": "sha512-6wlCNFFOcOcRa8GpuCzg0dhOcRMipONMZidP3jYUF0g7YkhknKxG4aA0BdmeH2Tp3Hm0286egicslGW9d7fZoA==",
"requires": {
"@photostructure/tz-lookup": "^8.0.0",
- "@types/luxon": "^3.3.2",
+ "@types/luxon": "^3.3.5",
"batch-cluster": "^12.1.0",
- "exiftool-vendored.exe": "12.67.0",
- "exiftool-vendored.pl": "12.67.0",
+ "exiftool-vendored.exe": "12.70.0",
+ "exiftool-vendored.pl": "12.70.0",
"he": "^1.2.0",
- "luxon": "^3.4.3"
- },
- "dependencies": {
- "exiftool-vendored.pl": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz",
- "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==",
- "optional": true
- }
+ "luxon": "^3.4.4"
}
},
"exiftool-vendored.exe": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.67.0.tgz",
- "integrity": "sha512-wzgMDoL/VWH34l38g22cVUn43mVFtTSVj0HRjfjR46+4fGwpSvSueeYbwLCZ5NvBAVINCS5Rz9Rl2DVmqoIjsw==",
+ "version": "12.70.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.70.0.tgz",
+ "integrity": "sha512-Jp78fvWCls2q3+6P0GxEpgBEgIj2MJsrOKxfAnVzADO3BZ8jRTdYCHVMUoxbiuzIcqzLmQA2mPLjhQ35Y4MyeA==",
"optional": true
},
"exiftool-vendored.pl": {
- "version": "12.67.0",
- "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz",
- "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA=="
+ "version": "12.70.0",
+ "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.70.0.tgz",
+ "integrity": "sha512-nozVKVE7Leq2lQ+Kd6VbX+S04z0TKYGd42F1odwEl8AtWNlJViZ7rsi+OLHkJ0llWmObotviYAsj/xhF28qPSQ=="
},
"exit": {
"version": "0.1.2",
@@ -17606,21 +17371,6 @@
"bser": "2.1.1"
}
},
- "fdir": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz",
- "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==",
- "requires": {}
- },
- "fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "requires": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- }
- },
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -17806,14 +17556,6 @@
"mime-types": "^2.1.12"
}
},
- "formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "requires": {
- "fetch-blob": "^3.1.2"
- }
- },
"formidable": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
@@ -18195,86 +17937,6 @@
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true
},
- "immich": {
- "version": "0.41.0",
- "resolved": "https://registry.npmjs.org/immich/-/immich-0.41.0.tgz",
- "integrity": "sha512-F+DIJ41/1L4Cqs0NqubcD2qttYcxTs32DCZS2l6U7d+a4SVtbxYwtEkvljSKACka1vokmHetpveGyRau2FdZ8w==",
- "requires": {
- "axios": "^0.26.0",
- "chalk": "^2.4.1",
- "cli-progress": "^3.10.0",
- "commander": "^9.0.0",
- "fdir": "^5.2.0",
- "form-data": "^4.0.0",
- "mime-types": "^2.1.34",
- "p-limit": "3.1.0",
- "systeminformation": "^5.11.6"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "requires": {
- "color-convert": "^1.9.0"
- }
- },
- "axios": {
- "version": "0.26.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
- "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
- "requires": {
- "follow-redirects": "^1.14.8"
- }
- },
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- }
- },
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "requires": {
- "color-name": "1.1.3"
- }
- },
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
- },
- "commander": {
- "version": "9.5.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
- "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
- },
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
- }
- },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -19174,11 +18836,6 @@
"universalify": "^2.0.0"
}
},
- "kdt": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz",
- "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg=="
- },
"keyv": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
@@ -19263,31 +18920,6 @@
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"dev": true
},
- "local-reverse-geocoder": {
- "version": "0.16.5",
- "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz",
- "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==",
- "requires": {
- "async": "^3.2.4",
- "csv-parse": "^5.5.0",
- "debug": "^4.3.4",
- "kdt": "^0.1.0",
- "node-fetch": "^3.3.2",
- "unzip-stream": "^0.3.1"
- },
- "dependencies": {
- "node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "requires": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- }
- }
- }
- },
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -19768,11 +19400,6 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
- "node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
- },
"node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@@ -19982,6 +19609,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
@@ -20188,7 +19816,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "devOptional": true
+ "dev": true
},
"pirates": {
"version": "4.0.6",
@@ -21330,11 +20958,6 @@
"tslib": "^2.5.0"
}
},
- "systeminformation": {
- "version": "5.21.9",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz",
- "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA=="
- },
"tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -21474,12 +21097,13 @@
}
},
"testcontainers": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.2.tgz",
- "integrity": "sha512-5GZ93rtoVXMx/s3xZjydftrKLnv1Yf+ETzGkXYRCm16LB60W48SGodxuiouYvNlVy0y0ogoQhdOw3DqsPActEA==",
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz",
+ "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==",
"dev": true,
"requires": {
"@balena/dockerignore": "^1.0.2",
+ "@types/dockerode": "^3.3.21",
"archiver": "^5.3.2",
"async-lock": "^1.4.0",
"byline": "^5.0.0",
@@ -21742,11 +21366,6 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
- "traverse": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
- "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
- },
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -22003,9 +21622,9 @@
}
},
"typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"devOptional": true
},
"typesense": {
@@ -22073,15 +21692,6 @@
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
"dev": true
},
- "unzip-stream": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz",
- "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==",
- "requires": {
- "binary": "^0.3.0",
- "mkdirp": "^0.5.1"
- }
- },
"update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -22201,11 +21811,6 @@
"defaults": "^1.0.3"
}
},
- "web-streams-polyfill": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
- "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
- },
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -22417,6 +22022,11 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
+ "yaml": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
+ "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="
+ },
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -22452,7 +22062,8 @@
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
},
"zip-stream": {
"version": "5.0.1",
diff --git a/server/package.json b/server/package.json
index 2e49d17a3..142cb92e1 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "immich",
- "version": "1.87.0",
+ "version": "1.89.0",
"description": "",
"author": "",
"private": true,
@@ -40,6 +40,7 @@
},
"dependencies": {
"@babel/runtime": "^7.22.11",
+ "@immich/cli": "^2.0.3",
"@nestjs/bullmq": "^10.0.1",
"@nestjs/common": "^10.2.2",
"@nestjs/config": "^3.0.0",
@@ -58,17 +59,15 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
- "exiftool-vendored": "~23.1.0",
- "exiftool-vendored.pl": "12.67",
+ "exiftool-vendored": "~23.5.0",
+ "exiftool-vendored.pl": "12.70",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^7.0.7",
"glob": "^10.3.3",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
- "immich": "^0.41.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
- "local-reverse-geocoder": "0.16.5",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
"mv": "^2.1.1",
@@ -90,14 +89,14 @@
"@nestjs/testing": "^10.2.2",
"@openapitools/openapi-generator-cli": "2.7.0",
"@testcontainers/postgresql": "^10.2.1",
- "@types/archiver": "^5.3.2",
+ "@types/archiver": "^6.0.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
- "@types/jest": "29.5.8",
+ "@types/jest": "29.5.10",
"@types/jest-when": "^3.5.2",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
diff --git a/server/assets/style-dark.json b/server/resources/style-dark.json
similarity index 100%
rename from server/assets/style-dark.json
rename to server/resources/style-dark.json
diff --git a/server/assets/style-light.json b/server/resources/style-light.json
similarity index 100%
rename from server/assets/style-light.json
rename to server/resources/style-light.json
diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts
index 16527de98..c47b2acd2 100644
--- a/server/src/domain/access/access.core.ts
+++ b/server/src/domain/access/access.core.ts
@@ -1,5 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthUserDto } from '../auth';
+import { setDifference, setUnion } from '../domain.util';
import { IAccessRepository } from '../repositories';
export enum Permission {
@@ -68,40 +69,66 @@ export class AccessCore {
return authUser;
}
+ /**
+ * Check if user has access to all ids, for the given permission.
+ * Throws error if user does not have access to any of the ids.
+ */
async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
- const hasAccess = await this.hasPermission(authUser, permission, ids);
- if (!hasAccess) {
+ ids = Array.isArray(ids) ? ids : [ids];
+ const allowedIds = await this.checkAccess(authUser, permission, ids);
+ if (new Set(ids).size !== allowedIds.size) {
throw new BadRequestException(`Not found or no ${permission} access`);
}
}
- async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) {
- for (const { permission, id } of permissions) {
- const hasAccess = await this.hasPermission(authUser, permission, id);
- if (hasAccess) {
- return true;
- }
+ /**
+ * Return ids that user has access to, for the given permission.
+ * Check is done for each id, and only allowed ids are returned.
+ *
+ * @returns Set
+ */
+ async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set | string[]) {
+ const idSet = Array.isArray(ids) ? new Set(ids) : ids;
+ if (idSet.size === 0) {
+ return new Set();
}
- return false;
- }
-
- async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
- ids = Array.isArray(ids) ? ids : [ids];
const isSharedLink = authUser.isPublicUser ?? false;
-
- for (const id of ids) {
- const hasAccess = isSharedLink
- ? await this.hasSharedLinkAccess(authUser, permission, id)
- : await this.hasOtherAccess(authUser, permission, id);
- if (!hasAccess) {
- return false;
- }
- }
-
- return true;
+ return isSharedLink
+ ? await this.checkAccessSharedLink(authUser, permission, idSet)
+ : await this.checkAccessOther(authUser, permission, idSet);
}
+ private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set) {
+ const sharedLinkId = authUser.sharedLinkId;
+ if (!sharedLinkId) {
+ return new Set();
+ }
+
+ switch (permission) {
+ case Permission.ASSET_UPLOAD:
+ return authUser.isAllowUpload ? ids : new Set();
+
+ case Permission.ALBUM_READ:
+ return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
+
+ case Permission.ALBUM_DOWNLOAD:
+ return !!authUser.isAllowDownload
+ ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
+ : new Set();
+ }
+
+ const allowedIds = new Set();
+ for (const id of ids) {
+ const hasAccess = await this.hasSharedLinkAccess(authUser, permission, id);
+ if (hasAccess) {
+ allowedIds.add(id);
+ }
+ }
+ return allowedIds;
+ }
+
+ // TODO: Migrate logic to checkAccessSharedLink to evaluate permissions in bulk.
private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) {
const sharedLinkId = authUser.sharedLinkId;
if (!sharedLinkId) {
@@ -118,24 +145,95 @@ export class AccessCore {
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id));
- case Permission.ASSET_UPLOAD:
- return authUser.isAllowUpload;
-
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return this.repository.asset.hasOwnerAccess(authUser.id, id);
- case Permission.ALBUM_READ:
- return this.repository.album.hasSharedLinkAccess(sharedLinkId, id);
-
- case Permission.ALBUM_DOWNLOAD:
- return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id));
-
default:
return false;
}
}
+ private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set) {
+ switch (permission) {
+ case Permission.ALBUM_READ: {
+ const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
+ const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
+ return setUnion(isOwner, isShared);
+ }
+
+ case Permission.ALBUM_UPDATE:
+ return this.repository.album.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.ALBUM_DELETE:
+ return this.repository.album.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.ALBUM_SHARE:
+ return this.repository.album.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.ALBUM_DOWNLOAD: {
+ const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
+ const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
+ return setUnion(isOwner, isShared);
+ }
+
+ case Permission.ALBUM_REMOVE_ASSET:
+ return this.repository.album.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.ASSET_UPLOAD:
+ return this.repository.library.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.ARCHIVE_READ:
+ return ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
+
+ case Permission.AUTH_DEVICE_DELETE:
+ return this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.TIMELINE_READ: {
+ const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
+ const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
+ return setUnion(isOwner, isPartner);
+ }
+
+ case Permission.TIMELINE_DOWNLOAD:
+ return ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
+
+ case Permission.LIBRARY_READ: {
+ const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids);
+ const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
+ return setUnion(isOwner, isPartner);
+ }
+
+ case Permission.LIBRARY_UPDATE:
+ return this.repository.library.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.LIBRARY_DELETE:
+ return this.repository.library.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.PERSON_READ:
+ return this.repository.person.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.PERSON_WRITE:
+ return this.repository.person.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.PERSON_MERGE:
+ return this.repository.person.checkOwnerAccess(authUser.id, ids);
+
+ case Permission.PARTNER_UPDATE:
+ return this.repository.partner.checkUpdateAccess(authUser.id, ids);
+ }
+
+ const allowedIds = new Set();
+ for (const id of ids) {
+ const hasAccess = await this.hasOtherAccess(authUser, permission, id);
+ if (hasAccess) {
+ allowedIds.add(id);
+ }
+ }
+ return allowedIds;
+ }
+
+ // TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk.
private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) {
switch (permission) {
// uses album id
@@ -184,69 +282,6 @@ export class AccessCore {
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
);
- case Permission.ALBUM_READ:
- return (
- (await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
- (await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
- );
-
- case Permission.ALBUM_UPDATE:
- return this.repository.album.hasOwnerAccess(authUser.id, id);
-
- case Permission.ALBUM_DELETE:
- return this.repository.album.hasOwnerAccess(authUser.id, id);
-
- case Permission.ALBUM_SHARE:
- return this.repository.album.hasOwnerAccess(authUser.id, id);
-
- case Permission.ALBUM_DOWNLOAD:
- return (
- (await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
- (await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
- );
-
- case Permission.ASSET_UPLOAD:
- return this.repository.library.hasOwnerAccess(authUser.id, id);
-
- case Permission.ALBUM_REMOVE_ASSET:
- return this.repository.album.hasOwnerAccess(authUser.id, id);
-
- case Permission.ARCHIVE_READ:
- return authUser.id === id;
-
- case Permission.AUTH_DEVICE_DELETE:
- return this.repository.authDevice.hasOwnerAccess(authUser.id, id);
-
- case Permission.TIMELINE_READ:
- return authUser.id === id || (await this.repository.timeline.hasPartnerAccess(authUser.id, id));
-
- case Permission.TIMELINE_DOWNLOAD:
- return authUser.id === id;
-
- case Permission.LIBRARY_READ:
- return (
- (await this.repository.library.hasOwnerAccess(authUser.id, id)) ||
- (await this.repository.library.hasPartnerAccess(authUser.id, id))
- );
-
- case Permission.LIBRARY_UPDATE:
- return this.repository.library.hasOwnerAccess(authUser.id, id);
-
- case Permission.LIBRARY_DELETE:
- return this.repository.library.hasOwnerAccess(authUser.id, id);
-
- case Permission.PERSON_READ:
- return this.repository.person.hasOwnerAccess(authUser.id, id);
-
- case Permission.PERSON_WRITE:
- return this.repository.person.hasOwnerAccess(authUser.id, id);
-
- case Permission.PERSON_MERGE:
- return this.repository.person.hasOwnerAccess(authUser.id, id);
-
- case Permission.PARTNER_UPDATE:
- return this.repository.partner.hasUpdateAccess(authUser.id, id);
-
default:
return false;
}
diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/domain/activity/activity.spec.ts
index 496d8978b..659718bed 100644
--- a/server/src/domain/activity/activity.spec.ts
+++ b/server/src/domain/activity/activity.spec.ts
@@ -24,7 +24,7 @@ describe(ActivityService.name, () => {
describe('getAll', () => {
it('should get all', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
activityMock.search.mockResolvedValue([]);
await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]);
@@ -37,7 +37,7 @@ describe(ActivityService.name, () => {
});
it('should filter by type=like', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
activityMock.search.mockResolvedValue([]);
await expect(
@@ -52,7 +52,7 @@ describe(ActivityService.name, () => {
});
it('should filter by type=comment', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
activityMock.search.mockResolvedValue([]);
await expect(
@@ -70,7 +70,7 @@ describe(ActivityService.name, () => {
describe('getStatistics', () => {
it('should get the comment count', async () => {
activityMock.getStatistics.mockResolvedValue(1);
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId]));
await expect(
sut.getStatistics(authStub.admin, {
assetId: 'asset-id',
@@ -82,7 +82,6 @@ describe(ActivityService.name, () => {
describe('addComment', () => {
it('should require access to the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.create(authStub.admin, {
albumId: 'album-id',
@@ -114,7 +113,7 @@ describe(ActivityService.name, () => {
});
it('should fail because activity is disabled for the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
accessMock.activity.hasCreateAccess.mockResolvedValue(false);
activityMock.create.mockResolvedValue(activityStub.oneComment);
@@ -148,7 +147,7 @@ describe(ActivityService.name, () => {
});
it('should skip if like exists', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
activityMock.search.mockResolvedValue([activityStub.liked]);
diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts
index a93cb0ad1..e89030538 100644
--- a/server/src/domain/album/album.service.spec.ts
+++ b/server/src/domain/album/album.service.spec.ts
@@ -58,9 +58,9 @@ describe(AlbumService.name, () => {
describe('getAll', () => {
it('gets list of albums for auth user', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
- albumMock.getAssetCountForIds.mockResolvedValue([
- { albumId: albumStub.empty.id, assetCount: 0 },
- { albumId: albumStub.sharedWithUser.id, assetCount: 0 },
+ albumMock.getMetadataForIds.mockResolvedValue([
+ { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
+ { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
@@ -72,7 +72,14 @@ describe(AlbumService.name, () => {
it('gets list of albums that have a specific asset', async () => {
albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
- albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAsset.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
+ ]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
@@ -83,7 +90,9 @@ describe(AlbumService.name, () => {
it('gets list of albums that are shared', async () => {
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
- albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
+ albumMock.getMetadataForIds.mockResolvedValue([
+ { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined },
+ ]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: true });
@@ -94,7 +103,9 @@ describe(AlbumService.name, () => {
it('gets list of albums that are NOT shared', async () => {
albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
- albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
+ albumMock.getMetadataForIds.mockResolvedValue([
+ { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined },
+ ]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: false });
@@ -106,7 +117,14 @@ describe(AlbumService.name, () => {
it('counts assets correctly', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
- albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAsset.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
+ ]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, {});
@@ -118,8 +136,13 @@ describe(AlbumService.name, () => {
it('updates the album thumbnail by listing all albums', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
- albumMock.getAssetCountForIds.mockResolvedValue([
- { albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAssetInvalidThumbnail.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
@@ -134,8 +157,13 @@ describe(AlbumService.name, () => {
it('removes the thumbnail for an empty album', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
- albumMock.getAssetCountForIds.mockResolvedValue([
- { albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.emptyWithInvalidThumbnail.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
@@ -204,7 +232,6 @@ describe(AlbumService.name, () => {
});
it('should prevent updating a not owned album (shared with auth user)', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.update(authStub.admin, albumStub.sharedWithAdmin.id, {
albumName: 'new album name',
@@ -213,7 +240,7 @@ describe(AlbumService.name, () => {
});
it('should require a valid thumbnail asset id', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
albumMock.update.mockResolvedValue(albumStub.oneAsset);
albumMock.hasAsset.mockResolvedValue(false);
@@ -229,7 +256,7 @@ describe(AlbumService.name, () => {
});
it('should allow the owner to update the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
albumMock.update.mockResolvedValue(albumStub.oneAsset);
@@ -252,7 +279,7 @@ describe(AlbumService.name, () => {
describe('delete', () => {
it('should throw an error for an album not found', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(null);
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
@@ -263,7 +290,6 @@ describe(AlbumService.name, () => {
});
it('should not let a shared user delete the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
@@ -274,7 +300,7 @@ describe(AlbumService.name, () => {
});
it('should let the owner delete an album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id]));
albumMock.getById.mockResolvedValue(albumStub.empty);
await sut.delete(authStub.admin, albumStub.empty.id);
@@ -286,7 +312,6 @@ describe(AlbumService.name, () => {
describe('addUsers', () => {
it('should throw an error if the auth user is not the owner', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-1'] }),
).rejects.toBeInstanceOf(BadRequestException);
@@ -294,7 +319,7 @@ describe(AlbumService.name, () => {
});
it('should throw an error if the userId is already added', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
await expect(
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }),
@@ -303,7 +328,7 @@ describe(AlbumService.name, () => {
});
it('should throw an error if the userId does not exist', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
userMock.get.mockResolvedValue(null);
await expect(
@@ -313,7 +338,7 @@ describe(AlbumService.name, () => {
});
it('should add valid shared users', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
userMock.get.mockResolvedValue(userStub.user2);
@@ -328,14 +353,14 @@ describe(AlbumService.name, () => {
describe('removeUser', () => {
it('should require a valid album id', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
albumMock.getById.mockResolvedValue(null);
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled();
});
it('should remove a shared user from an owned album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id]));
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
await expect(
@@ -352,7 +377,6 @@ describe(AlbumService.name, () => {
});
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple);
await expect(
@@ -360,7 +384,10 @@ describe(AlbumService.name, () => {
).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled();
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, albumStub.sharedWithMultiple.id);
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
+ authStub.user1.id,
+ new Set([albumStub.sharedWithMultiple.id]),
+ );
});
it('should allow a shared user to remove themselves', async () => {
@@ -413,51 +440,75 @@ describe(AlbumService.name, () => {
describe('getAlbumInfo', () => {
it('should get a shared album', async () => {
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAsset.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
+ ]);
await sut.get(authStub.admin, albumStub.oneAsset.id, {});
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id);
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
+ authStub.admin.id,
+ new Set([albumStub.oneAsset.id]),
+ );
});
it('should get a shared album via a shared link', async () => {
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
- accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
+ accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAsset.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
+ ]);
await sut.get(authStub.adminSharedLink, 'album-123', {});
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
- expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
+ expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
- 'album-123',
+ new Set(['album-123']),
);
});
it('should get a shared album via shared with user', async () => {
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
+ accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
+ albumMock.getMetadataForIds.mockResolvedValue([
+ {
+ albumId: albumStub.oneAsset.id,
+ assetCount: 1,
+ startDate: new Date('1970-01-01'),
+ endDate: new Date('1970-01-01'),
+ },
+ ]);
await sut.get(authStub.user1, 'album-123', {});
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
- expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123');
+ expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['album-123']));
});
it('should throw an error for no access', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
-
await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123');
- expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123');
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123']));
+ expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123']));
});
});
describe('addAssets', () => {
it('should allow the owner to add assets', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@@ -482,7 +533,7 @@ describe(AlbumService.name, () => {
});
it('should not set the thumbnail if the album has one already', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@@ -500,8 +551,7 @@ describe(AlbumService.name, () => {
});
it('should allow a shared user to add assets', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
+ accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@@ -526,9 +576,7 @@ describe(AlbumService.name, () => {
});
it('should allow a shared link user to add assets', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
- accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
+ accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@@ -551,14 +599,14 @@ describe(AlbumService.name, () => {
assetIds: ['asset-1', 'asset-2', 'asset-3'],
});
- expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
+ expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
- 'album-123',
+ new Set(['album-123']),
);
});
it('should allow adding assets shared via partner sharing', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
@@ -577,7 +625,7 @@ describe(AlbumService.name, () => {
});
it('should skip duplicate assets', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
@@ -590,7 +638,7 @@ describe(AlbumService.name, () => {
});
it('should skip assets not shared with user', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
@@ -605,33 +653,31 @@ describe(AlbumService.name, () => {
});
it('should not allow unauthorized access to the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
await expect(
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalled();
- expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalled();
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalled();
+ expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalled();
});
it('should not allow unauthorized shared link access to the album', async () => {
- accessMock.album.hasSharedLinkAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
await expect(
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalled();
+ expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalled();
});
});
describe('removeAssets', () => {
it('should allow the owner to remove assets', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123']));
+ accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
@@ -644,7 +690,7 @@ describe(AlbumService.name, () => {
});
it('should skip assets not in the album', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@@ -656,7 +702,7 @@ describe(AlbumService.name, () => {
});
it('should skip assets without user permission to remove', async () => {
- accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
+ accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
@@ -672,7 +718,8 @@ describe(AlbumService.name, () => {
});
it('should reset the thumbnail if it is removed', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123']));
+ accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts
index 37d44c33a..308735d43 100644
--- a/server/src/domain/album/album.service.ts
+++ b/server/src/domain/album/album.service.ts
@@ -3,8 +3,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset';
import { AuthUserDto } from '../auth';
+import { setUnion } from '../domain.util';
import { JobName } from '../job';
import {
+ AlbumAssetCount,
AlbumInfoOptions,
IAccessRepository,
IAlbumRepository,
@@ -68,11 +70,19 @@ export class AlbumService {
// Get asset count for each album. Then map the result to an object:
// { [albumId]: assetCount }
- const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id));
- const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record, { albumId, assetCount }) => {
- obj[albumId] = assetCount;
- return obj;
- }, {});
+ const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
+ const albumMetadataForIdsObj: Record = albumMetadataForIds.reduce(
+ (obj: Record, { albumId, assetCount, startDate, endDate }) => {
+ obj[albumId] = {
+ albumId,
+ assetCount,
+ startDate,
+ endDate,
+ };
+ return obj;
+ },
+ {},
+ );
return Promise.all(
albums.map(async (album) => {
@@ -80,17 +90,28 @@ export class AlbumService {
return {
...mapAlbumWithoutAssets(album),
sharedLinks: undefined,
- assetCount: albumsAssetCountObj[album.id],
+ startDate: albumMetadataForIdsObj[album.id].startDate,
+ endDate: albumMetadataForIdsObj[album.id].endDate,
+ assetCount: albumMetadataForIdsObj[album.id].assetCount,
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
};
}),
);
}
- async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto) {
+ async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails();
- return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets);
+ const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
+ const album = await this.findOrFail(id, { withAssets });
+ const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]);
+
+ return {
+ ...mapAlbum(album, withAssets),
+ startDate: albumMetadataForIds.startDate,
+ endDate: albumMetadataForIds.endDate,
+ assetCount: albumMetadataForIds.assetCount,
+ };
}
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise {
@@ -153,6 +174,8 @@ export class AlbumService {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
+ const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id));
+ const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
const results: BulkIdResponseDto[] = [];
for (const assetId of dto.ids) {
@@ -162,7 +185,7 @@ export class AlbumService {
continue;
}
- const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
+ const hasAccess = allowedAssetIds.has(assetId);
if (!hasAccess) {
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
continue;
@@ -190,6 +213,9 @@ export class AlbumService {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
+ const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
+ const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds);
+ const allowedAssetIds = setUnion(canRemove, canShare);
const results: BulkIdResponseDto[] = [];
for (const assetId of dto.ids) {
@@ -199,10 +225,7 @@ export class AlbumService {
continue;
}
- const hasAccess = await this.access.hasAny(authUser, [
- { permission: Permission.ALBUM_REMOVE_ASSET, id: assetId },
- { permission: Permission.ASSET_SHARE, id: assetId },
- ]);
+ const hasAccess = allowedAssetIds.has(assetId);
if (!hasAccess) {
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
continue;
diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts
index 45687282f..28a138254 100644
--- a/server/src/domain/asset/asset.service.spec.ts
+++ b/server/src/domain/asset/asset.service.spec.ts
@@ -347,14 +347,14 @@ describe(AssetService.name, () => {
describe('getTimeBucket', () => {
it('should return the assets for a album time bucket if user has album.read', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
await expect(
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-id');
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-id']));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
@@ -546,7 +546,7 @@ describe(AssetService.name, () => {
});
it('should return a list of archives (albumId)', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
assetMock.getByAlbumId.mockResolvedValue({
items: [assetStub.image, assetStub.video],
hasNextPage: false,
@@ -554,12 +554,12 @@ describe(AssetService.name, () => {
await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse);
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-1');
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-1']));
expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
});
it('should return a list of archives (userId)', async () => {
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
assetMock.getByUserId.mockResolvedValue({
items: [assetStub.image, assetStub.video],
hasNextPage: false,
@@ -575,7 +575,7 @@ describe(AssetService.name, () => {
});
it('should split archives by size', async () => {
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
assetMock.getByUserId.mockResolvedValue({
items: [
@@ -1067,4 +1067,18 @@ describe(AssetService.name, () => {
);
});
});
+
+ it('get assets by device id', async () => {
+ const assets = [assetStub.image, assetStub.image1];
+
+ assetMock.getAllByDeviceId.mockImplementation(() =>
+ Promise.resolve(Array.from(assets.map((asset) => asset.deviceAssetId))),
+ );
+
+ const deviceId = 'device-id';
+ const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
+
+ expect(result.length).toEqual(2);
+ expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
+ });
});
diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts
index dbba670e7..86e480932 100644
--- a/server/src/domain/asset/asset.service.ts
+++ b/server/src/domain/asset/asset.service.ts
@@ -386,6 +386,10 @@ export class AssetService {
return assets.map((a) => mapAsset(a));
}
+ async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
+ return this.assetRepository.getAllByDeviceId(authUser.id, deviceId);
+ }
+
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts
index a815e22d1..7ece7bed8 100644
--- a/server/src/domain/auth/auth.service.spec.ts
+++ b/server/src/domain/auth/auth.service.spec.ts
@@ -395,11 +395,11 @@ describe('AuthService', () => {
describe('logoutDevice', () => {
it('should logout the device', async () => {
- accessMock.authDevice.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
await sut.logoutDevice(authStub.user1, 'token-1');
- expect(accessMock.authDevice.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, 'token-1');
+ expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1']));
expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
});
});
diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts
index 0db2b51bf..016325430 100644
--- a/server/src/domain/domain.constant.spec.ts
+++ b/server/src/domain/domain.constant.spec.ts
@@ -7,6 +7,7 @@ describe('mimeTypes', () => {
{ mimetype: 'image/ari', extension: '.ari' },
{ mimetype: 'image/arw', extension: '.arw' },
{ mimetype: 'image/avif', extension: '.avif' },
+ { mimetype: 'image/bmp', extension: '.bmp' },
{ mimetype: 'image/cap', extension: '.cap' },
{ mimetype: 'image/cin', extension: '.cin' },
{ mimetype: 'image/cr2', extension: '.cr2' },
diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts
index 1b301be56..a4c17ff2d 100644
--- a/server/src/domain/domain.constant.ts
+++ b/server/src/domain/domain.constant.ts
@@ -63,6 +63,7 @@ const image: Record = {
'.ari': ['image/ari', 'image/x-arriflex-ari'],
'.arw': ['image/arw', 'image/x-sony-arw'],
'.avif': ['image/avif'],
+ '.bmp': ['image/bmp'],
'.cap': ['image/cap', 'image/x-phaseone-cap'],
'.cin': ['image/cin', 'image/x-phantom-cin'],
'.cr2': ['image/cr2', 'image/x-canon-cr2'],
diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts
index 53e674bf3..00ad27bc7 100644
--- a/server/src/domain/domain.util.ts
+++ b/server/src/domain/domain.util.ts
@@ -150,3 +150,23 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = {
return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions);
}
+
+// NOTE: The following Set utils have been added here, to easily determine where they are used.
+// They should be replaced with native Set operations, when they are added to the language.
+// Proposal reference: https://github.com/tc39/proposal-set-methods
+
+export const setUnion = (setA: Set, setB: Set): Set => {
+ const union = new Set(setA);
+ for (const elem of setB) {
+ union.add(elem);
+ }
+ return union;
+};
+
+export const setDifference = (setA: Set, setB: Set): Set => {
+ const difference = new Set(setA);
+ for (const elem of setB) {
+ difference.delete(elem);
+ }
+ return difference;
+};
diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts
index 3d7d68736..c7e15e960 100644
--- a/server/src/domain/library/library.service.spec.ts
+++ b/server/src/domain/library/library.service.spec.ts
@@ -58,7 +58,7 @@ describe(LibraryService.name, () => {
ctime: new Date('2023-01-01'),
} as Stats);
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
sut = new LibraryService(
accessMock,
diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts
index f31605b12..7ce7db054 100644
--- a/server/src/domain/metadata/metadata.service.spec.ts
+++ b/server/src/domain/metadata/metadata.service.spec.ts
@@ -1,4 +1,4 @@
-import { AssetType, CitiesFile, ExifEntity, SystemConfigKey } from '@app/infra/entities';
+import { AssetType, ExifEntity, SystemConfigKey } from '@app/infra/entities';
import {
assetStub,
newAlbumRepositoryMock,
@@ -14,7 +14,8 @@ import {
import { randomBytes } from 'crypto';
import { Stats } from 'fs';
import { constants } from 'fs/promises';
-import { JobName, QueueName } from '../job';
+import { when } from 'jest-when';
+import { JobName } from '../job';
import {
IAlbumRepository,
IAssetRepository,
@@ -77,10 +78,7 @@ describe(MetadataService.name, () => {
describe('init', () => {
beforeEach(async () => {
- configMock.load.mockResolvedValue([
- { key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true },
- { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_500 },
- ]);
+ configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
await sut.init();
});
@@ -89,42 +87,10 @@ describe(MetadataService.name, () => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]);
await sut.init();
- expect(metadataMock.deleteCache).not.toHaveBeenCalled();
expect(jobMock.pause).toHaveBeenCalledTimes(1);
expect(metadataMock.init).toHaveBeenCalledTimes(1);
expect(jobMock.resume).toHaveBeenCalledTimes(1);
});
-
- it('should return if deleteCache is false and the cities precision has not changed', async () => {
- await sut.init();
-
- expect(metadataMock.deleteCache).not.toHaveBeenCalled();
- expect(jobMock.pause).toHaveBeenCalledTimes(1);
- expect(metadataMock.init).toHaveBeenCalledTimes(1);
- expect(jobMock.resume).toHaveBeenCalledTimes(1);
- });
-
- it('should re-init if deleteCache is false but the cities precision has changed', async () => {
- configMock.load.mockResolvedValue([
- { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_1000 },
- ]);
-
- await sut.init();
-
- expect(metadataMock.deleteCache).not.toHaveBeenCalled();
- expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
- expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 });
- expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
- });
-
- it('should re-init and delete cache if deleteCache is true', async () => {
- await sut.init(true);
-
- expect(metadataMock.deleteCache).toHaveBeenCalled();
- expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
- expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 });
- expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
- });
});
describe('handleLivePhotoLinking', () => {
@@ -248,6 +214,30 @@ describe(MetadataService.name, () => {
expect(assetMock.save).not.toHaveBeenCalled();
});
+ it('should handle a date in a sidecar file', async () => {
+ const originalDate = new Date('2023-11-21T16:13:17.517Z');
+ const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
+ assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
+ when(metadataMock.getExifTags)
+ .calledWith(assetStub.sidecar.originalPath)
+ // higher priority tag
+ .mockResolvedValue({ CreationDate: originalDate.toISOString() });
+ when(metadataMock.getExifTags)
+ .calledWith(assetStub.sidecar.sidecarPath as string)
+ // lower priority tag, but in sidecar
+ .mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
+
+ await sut.handleMetadataExtraction({ id: assetStub.image.id });
+ expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
+ expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
+ expect(assetMock.save).toHaveBeenCalledWith({
+ id: assetStub.image.id,
+ duration: null,
+ fileCreatedAt: sidecarDate,
+ localDateTime: sidecarDate,
+ });
+ });
+
it('should handle lists of numbers', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });
diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts
index 45193c2e1..77dcfecb0 100644
--- a/server/src/domain/metadata/metadata.service.ts
+++ b/server/src/domain/metadata/metadata.service.ts
@@ -25,6 +25,18 @@ import {
import { StorageCore } from '../storage';
import { FeatureFlag, SystemConfigCore } from '../system-config';
+/** look for a date from these tags (in order) */
+const EXIF_DATE_TAGS: Array = [
+ 'SubSecDateTimeOriginal',
+ 'DateTimeOriginal',
+ 'SubSecCreateDate',
+ 'CreationDate',
+ 'CreateDate',
+ 'SubSecMediaCreateDate',
+ 'MediaCreateDate',
+ 'DateTimeCreated',
+];
+
interface DirectoryItem {
Length?: number;
Mime: string;
@@ -39,7 +51,7 @@ interface DirectoryEntry {
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<
ExifEntity,
'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn'
->;
+> & { dateTimeOriginal: Date };
const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null);
const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null);
@@ -85,31 +97,24 @@ export class MetadataService {
this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository);
}
- async init(deleteCache = false) {
+ async init() {
if (!this.subscription) {
this.subscription = this.configCore.config$.subscribe(() => this.init());
}
const { reverseGeocoding } = await this.configCore.getConfig();
- const { citiesFileOverride } = reverseGeocoding;
+ const { enabled } = reverseGeocoding;
- if (!reverseGeocoding.enabled) {
+ if (!enabled) {
return;
}
try {
- if (deleteCache) {
- await this.repository.deleteCache();
- } else if (this.oldCities && this.oldCities === citiesFileOverride) {
- return;
- }
-
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
- await this.repository.init({ citiesFileOverride });
+ await this.repository.init();
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
- this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`);
- this.oldCities = citiesFileOverride;
+ this.logger.log(`Initialized local reverse geocoder`);
} catch (error: Error | any) {
this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack);
}
@@ -181,7 +186,7 @@ export class MetadataService {
await this.applyReverseGeocoding(asset, exifData);
await this.assetRepository.upsertExif(exifData);
- const dateTimeOriginal = exifDate(firstDateTime(tags as Tags)) ?? exifData.dateTimeOriginal;
+ const dateTimeOriginal = exifData.dateTimeOriginal;
let localDateTime = dateTimeOriginal ?? undefined;
const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0;
@@ -246,8 +251,11 @@ export class MetadataService {
}
try {
- const { city, state, country } = await this.repository.reverseGeocode({ latitude, longitude });
- Object.assign(exifData, { city, state, country });
+ const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude });
+ if (!reverseGeocode) {
+ return;
+ }
+ Object.assign(exifData, reverseGeocode);
} catch (error: Error | any) {
this.logger.warn(
`Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
@@ -340,6 +348,15 @@ export class MetadataService {
const stats = await this.storageRepository.stat(asset.originalPath);
const mediaTags = await this.repository.getExifTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null;
+
+ // ensure date from sidecar is used if present
+ const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);
+ if (mediaTags && hasDateOverride) {
+ for (const tag of EXIF_DATE_TAGS) {
+ delete mediaTags[tag];
+ }
+ }
+
const tags = { ...mediaTags, ...sidecarTags };
this.logger.verbose('Exif Tags', tags);
@@ -350,19 +367,7 @@ export class MetadataService {
assetId: asset.id,
bitsPerSample: this.getBitsPerSample(tags),
colorspace: tags.ColorSpace ?? null,
- dateTimeOriginal:
- exifDate(
- firstDateTime(tags as Tags, [
- 'SubSecDateTimeOriginal',
- 'DateTimeOriginal',
- 'SubSecCreateDate',
- 'CreationDate',
- 'CreateDate',
- 'SubSecMediaCreateDate',
- 'MediaCreateDate',
- 'DateTimeCreated',
- ]),
- ) ?? asset.fileCreatedAt,
+ dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt,
exifImageHeight: validate(tags.ImageHeight),
exifImageWidth: validate(tags.ImageWidth),
exposureTime: tags.ExposureTime ?? null,
@@ -387,6 +392,13 @@ export class MetadataService {
};
}
+ private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
+ if (!tags) {
+ return null;
+ }
+ return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS));
+ }
+
private getBitsPerSample(tags: ImmichTags): number | null {
const bitDepthTags = [
tags.BitsPerSample,
diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts
index 3a4ac6b6d..b210a9165 100644
--- a/server/src/domain/person/person.service.spec.ts
+++ b/server/src/domain/person/person.service.spec.ts
@@ -183,105 +183,101 @@ describe(PersonService.name, () => {
describe('getById', () => {
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.withName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw a bad request when person is not found', async () => {
personMock.getById.mockResolvedValue(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should get a person by id', async () => {
personMock.getById.mockResolvedValue(personStub.withName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto);
expect(personMock.getById).toHaveBeenCalledWith('person-1');
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
describe('getThumbnail', () => {
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.noName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw an error when personId is invalid', async () => {
personMock.getById.mockResolvedValue(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw an error when person has no thumbnail', async () => {
personMock.getById.mockResolvedValue(personStub.noThumbnail);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should serve the thumbnail', async () => {
personMock.getById.mockResolvedValue(personStub.noName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getThumbnail(authStub.admin, 'person-1');
expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg');
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
describe('getAssets', () => {
it('should require person.read permission', async () => {
personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(personMock.getAssets).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it("should return a person's assets", async () => {
personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getAssets(authStub.admin, 'person-1');
expect(personMock.getAssets).toHaveBeenCalledWith('person-1');
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
describe('update', () => {
it('should require person.write permission', async () => {
personMock.getById.mockResolvedValue(personStub.noName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw an error when personId is invalid', async () => {
personMock.getById.mockResolvedValue(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it("should update a person's name", async () => {
personMock.getById.mockResolvedValue(personStub.noName);
personMock.update.mockResolvedValue(personStub.withName);
personMock.getAssets.mockResolvedValue([assetStub.image]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
@@ -291,14 +287,14 @@ describe(PersonService.name, () => {
name: JobName.SEARCH_INDEX_ASSET,
data: { ids: [assetStub.image.id] },
});
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it("should update a person's date of birth", async () => {
personMock.getById.mockResolvedValue(personStub.noBirthDate);
personMock.update.mockResolvedValue(personStub.withBirthDate);
personMock.getAssets.mockResolvedValue([assetStub.image]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({
id: 'person-1',
@@ -311,14 +307,14 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
expect(jobMock.queue).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should update a person visibility', async () => {
personMock.getById.mockResolvedValue(personStub.hidden);
personMock.update.mockResolvedValue(personStub.withName);
personMock.getAssets.mockResolvedValue([assetStub.image]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
@@ -328,7 +324,7 @@ describe(PersonService.name, () => {
name: JobName.SEARCH_INDEX_ASSET,
data: { ids: [assetStub.image.id] },
});
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it("should update a person's thumbnailPath", async () => {
@@ -336,7 +332,7 @@ describe(PersonService.name, () => {
personMock.update.mockResolvedValue(personStub.withName);
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
@@ -351,31 +347,31 @@ describe(PersonService.name, () => {
},
]);
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } });
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw an error when the face feature assetId is invalid', async () => {
personMock.getById.mockResolvedValue(personStub.withName);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow(
BadRequestException,
);
expect(personMock.update).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
describe('updateAll', () => {
it('should throw an error when personId is invalid', async () => {
personMock.getById.mockResolvedValue(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(
sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
expect(personMock.update).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
@@ -652,7 +648,6 @@ describe(PersonService.name, () => {
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
personMock.prepareReassignFaces.mockResolvedValue([]);
personMock.delete.mockResolvedValue(personStub.mergePerson);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
BadRequestException,
@@ -663,7 +658,7 @@ describe(PersonService.name, () => {
expect(personMock.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should merge two people', async () => {
@@ -671,7 +666,8 @@ describe(PersonService.name, () => {
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
personMock.prepareReassignFaces.mockResolvedValue([]);
personMock.delete.mockResolvedValue(personStub.mergePerson);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
{ id: 'person-2', success: true },
@@ -691,14 +687,15 @@ describe(PersonService.name, () => {
name: JobName.PERSON_DELETE,
data: { id: personStub.mergePerson.id },
});
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should delete conflicting faces before merging', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
personMock.getById.mockResolvedValue(personStub.mergePerson);
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
{ id: 'person-2', success: true },
@@ -713,25 +710,26 @@ describe(PersonService.name, () => {
name: JobName.SEARCH_REMOVE_FACE,
data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id },
});
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should throw an error when the primary person is not found', async () => {
personMock.getById.mockResolvedValue(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(personMock.delete).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should handle invalid merge ids', async () => {
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
personMock.getById.mockResolvedValueOnce(null);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
{ id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
@@ -740,7 +738,7 @@ describe(PersonService.name, () => {
expect(personMock.prepareReassignFaces).not.toHaveBeenCalled();
expect(personMock.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should handle an error reassigning faces', async () => {
@@ -748,14 +746,15 @@ describe(PersonService.name, () => {
personMock.getById.mockResolvedValue(personStub.mergePerson);
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
personMock.reassignFaces.mockRejectedValue(new Error('update failed'));
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
+ accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
{ id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN },
]);
expect(personMock.delete).not.toHaveBeenCalled();
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
@@ -763,16 +762,15 @@ describe(PersonService.name, () => {
it('should get correct number of person', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
personMock.getStatistics.mockResolvedValue(statistics);
- accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
- accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
- expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+ expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
});
});
});
diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts
index 49329a452..3452807f6 100644
--- a/server/src/domain/person/person.service.ts
+++ b/server/src/domain/person/person.service.ts
@@ -375,10 +375,11 @@ export class PersonService {
const results: BulkIdResponseDto[] = [];
- for (const mergeId of mergeIds) {
- const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId);
+ const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds);
- if (!hasPermission) {
+ for (const mergeId of mergeIds) {
+ const hasAccess = allowedIds.has(mergeId);
+ if (!hasAccess) {
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
continue;
}
diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts
index 9c009719d..7736fd890 100644
--- a/server/src/domain/repositories/access.repository.ts
+++ b/server/src/domain/repositories/access.repository.ts
@@ -14,29 +14,29 @@ export interface IAccessRepository {
};
authDevice: {
- hasOwnerAccess(userId: string, deviceId: string): Promise;
+ checkOwnerAccess(userId: string, deviceIds: Set): Promise>;
};
album: {
- hasOwnerAccess(userId: string, albumId: string): Promise;
- hasSharedAlbumAccess(userId: string, albumId: string): Promise;
- hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise;
+ checkOwnerAccess(userId: string, albumIds: Set): Promise>;
+ checkSharedAlbumAccess(userId: string, albumIds: Set): Promise>;
+ checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>;
};
library: {
- hasOwnerAccess(userId: string, libraryId: string): Promise;
- hasPartnerAccess(userId: string, partnerId: string): Promise;
+ checkOwnerAccess(userId: string, libraryIds: Set): Promise>;
+ checkPartnerAccess(userId: string, partnerIds: Set): Promise>;
};
timeline: {
- hasPartnerAccess(userId: string, partnerId: string): Promise;
+ checkPartnerAccess(userId: string, partnerIds: Set): Promise>;
};
person: {
- hasOwnerAccess(userId: string, personId: string): Promise;
+ checkOwnerAccess(userId: string, personIds: Set): Promise>;
};
partner: {
- hasUpdateAccess(userId: string, partnerId: string): Promise;
+ checkUpdateAccess(userId: string, partnerIds: Set): Promise>;
};
}
diff --git a/server/src/domain/repositories/album.repository.ts b/server/src/domain/repositories/album.repository.ts
index d3ca62da1..10b789b4b 100644
--- a/server/src/domain/repositories/album.repository.ts
+++ b/server/src/domain/repositories/album.repository.ts
@@ -5,6 +5,8 @@ export const IAlbumRepository = 'IAlbumRepository';
export interface AlbumAssetCount {
albumId: string;
assetCount: number;
+ startDate: Date | undefined;
+ endDate: Date | undefined;
}
export interface AlbumInfoOptions {
@@ -30,7 +32,7 @@ export interface IAlbumRepository {
hasAsset(asset: AlbumAsset): Promise;
removeAsset(assetId: string): Promise;
removeAssets(assets: AlbumAssets): Promise;
- getAssetCountForIds(ids: string[]): Promise;
+ getMetadataForIds(ids: string[]): Promise;
getInvalidThumbnail(): Promise;
getOwned(ownerId: string): Promise;
getShared(ownerId: string): Promise;
diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts
index 12ae49000..a42952958 100644
--- a/server/src/domain/repositories/asset.repository.ts
+++ b/server/src/domain/repositories/asset.repository.ts
@@ -162,6 +162,7 @@ export interface IAssetRepository {
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise;
deleteAll(ownerId: string): Promise;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated;
+ getAllByDeviceId(userId: string, deviceId: string): Promise;
updateAll(ids: string[], options: Partial): Promise;
save(asset: Pick & Partial): Promise;
remove(asset: AssetEntity): Promise;
diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts
index ff098d8db..f812e6ee5 100644
--- a/server/src/domain/repositories/index.ts
+++ b/server/src/domain/repositories/index.ts
@@ -20,6 +20,7 @@ export * from './shared-link.repository';
export * from './smart-info.repository';
export * from './storage.repository';
export * from './system-config.repository';
+export * from './system-metadata.repository';
export * from './tag.repository';
export * from './user-token.repository';
export * from './user.repository';
diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts
index 0c3b78462..c0a0fef46 100644
--- a/server/src/domain/repositories/metadata.repository.ts
+++ b/server/src/domain/repositories/metadata.repository.ts
@@ -1,5 +1,4 @@
import { Tags } from 'exiftool-vendored';
-import { InitOptions } from 'local-reverse-geocoder';
export const IMetadataRepository = 'IMetadataRepository';
@@ -31,9 +30,8 @@ export interface ImmichTags extends Omit {
}
export interface IMetadataRepository {
- init(options: Partial): Promise;
+ init(): Promise;
teardown(): Promise;
- reverseGeocode(point: GeoPoint): Promise;
- deleteCache(): Promise;
+ reverseGeocode(point: GeoPoint): Promise;
getExifTags(path: string): Promise;
}
diff --git a/server/src/domain/repositories/system-metadata.repository.ts b/server/src/domain/repositories/system-metadata.repository.ts
new file mode 100644
index 000000000..4d571953b
--- /dev/null
+++ b/server/src/domain/repositories/system-metadata.repository.ts
@@ -0,0 +1,8 @@
+import { SystemMetadata } from '@app/infra/entities';
+
+export const ISystemMetadataRepository = 'ISystemMetadataRepository';
+
+export interface ISystemMetadataRepository {
+ get(key: T): Promise;
+ set(key: T, value: SystemMetadata[T]): Promise;
+}
diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts
index 863e3a353..abf8128c4 100644
--- a/server/src/domain/shared-link/shared-link.service.spec.ts
+++ b/server/src/domain/shared-link/shared-link.service.spec.ts
@@ -97,7 +97,6 @@ describe(SharedLinkService.name, () => {
});
it('should not allow non-owners to create album shared links', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }),
).rejects.toBeInstanceOf(BadRequestException);
@@ -117,12 +116,15 @@ describe(SharedLinkService.name, () => {
});
it('should create an album shared link', async () => {
- accessMock.album.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
shareMock.create.mockResolvedValue(sharedLinkStub.valid);
await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
- expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id);
+ expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
+ authStub.admin.id,
+ new Set([albumStub.oneAsset.id]),
+ );
expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.ALBUM,
userId: authStub.admin.id,
diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts
index d3fd89661..bd9aaea6a 100644
--- a/server/src/domain/shared-link/shared-link.service.ts
+++ b/server/src/domain/shared-link/shared-link.service.ts
@@ -119,15 +119,19 @@ export class SharedLinkService {
throw new BadRequestException('Invalid shared link type');
}
+ const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
+ const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
+ const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds);
+
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {
- const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId);
+ const hasAsset = existingAssetIds.has(assetId);
if (hasAsset) {
results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
continue;
}
- const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
+ const hasAccess = allowedAssetIds.has(assetId);
if (!hasAccess) {
results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION });
continue;
diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
index be20a02c7..aa224ccc6 100644
--- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
+++ b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
@@ -1,12 +1,6 @@
-import { CitiesFile } from '@app/infra/entities';
-import { ApiProperty } from '@nestjs/swagger';
-import { IsBoolean, IsEnum } from 'class-validator';
+import { IsBoolean } from 'class-validator';
export class SystemConfigReverseGeocodingDto {
@IsBoolean()
enabled!: boolean;
-
- @IsEnum(CitiesFile)
- @ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' })
- citiesFileOverride!: CitiesFile;
}
diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts
index b3a030487..bfab4bb4f 100644
--- a/server/src/domain/system-config/system-config.core.ts
+++ b/server/src/domain/system-config/system-config.core.ts
@@ -1,6 +1,5 @@
import {
AudioCodec,
- CitiesFile,
Colorspace,
CQMode,
SystemConfig,
@@ -85,7 +84,6 @@ export const defaults = Object.freeze({
},
reverseGeocoding: {
enabled: true,
- citiesFileOverride: CitiesFile.CITIES_500,
},
oauth: {
enabled: false,
diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts
index cdeb552b0..6ff4ac5c4 100644
--- a/server/src/domain/system-config/system-config.service.spec.ts
+++ b/server/src/domain/system-config/system-config.service.spec.ts
@@ -1,6 +1,5 @@
import {
AudioCodec,
- CitiesFile,
Colorspace,
CQMode,
SystemConfig,
@@ -85,7 +84,6 @@ const updatedConfig = Object.freeze({
},
reverseGeocoding: {
enabled: true,
- citiesFileOverride: CitiesFile.CITIES_500,
},
oauth: {
autoLaunch: true,
diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts
index 5e9743ba5..c81c462e8 100644
--- a/server/src/domain/system-config/system-config.service.ts
+++ b/server/src/domain/system-config/system-config.service.ts
@@ -79,7 +79,7 @@ export class SystemConfigService {
return this.repository.fetchStyle(styleUrl);
}
- return JSON.parse(await this.repository.readFile(`./assets/style-${theme}.json`));
+ return JSON.parse(await this.repository.readFile(`./resources/style-${theme}.json`));
}
async getCustomCss(): Promise {
diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts
index e7a04564c..ad17ccf31 100644
--- a/server/src/immich/api-v1/asset/asset.controller.ts
+++ b/server/src/immich/api-v1/asset/asset.controller.ts
@@ -14,7 +14,7 @@ import {
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
-import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
+import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
@@ -147,9 +147,10 @@ export class AssetController {
}
/**
- * Get all asset of a device that are in the database, ID only.
+ * @deprecated Use /asset/device/:deviceId instead - Remove at 1.92 release
*/
@Get('/:deviceId')
+ @ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' })
getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
return this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
}
diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts
index cc2102766..11173b55f 100644
--- a/server/src/immich/api-v1/asset/asset.service.spec.ts
+++ b/server/src/immich/api-v1/asset/asset.service.spec.ts
@@ -130,7 +130,7 @@ describe('AssetService', () => {
const dto = _getCreateAssetDto();
assetRepositoryMock.create.mockResolvedValue(assetEntity);
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
@@ -150,7 +150,7 @@ describe('AssetService', () => {
assetRepositoryMock.create.mockRejectedValue(error);
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
@@ -167,7 +167,7 @@ describe('AssetService', () => {
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
- accessMock.library.hasOwnerAccess.mockResolvedValue(true);
+ accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
diff --git a/server/src/immich/api-v1/asset/dto/device-id.dto.ts b/server/src/immich/api-v1/asset/dto/device-id.dto.ts
index ff2f4163b..cae5f60c8 100644
--- a/server/src/immich/api-v1/asset/dto/device-id.dto.ts
+++ b/server/src/immich/api-v1/asset/dto/device-id.dto.ts
@@ -1,9 +1,7 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsUUID } from 'class-validator';
+import { IsNotEmpty, IsString } from 'class-validator';
export class DeviceIdDto {
@IsNotEmpty()
- @IsUUID('4')
- @ApiProperty({ format: 'uuid' })
+ @IsString()
deviceId!: string;
}
diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/immich/controllers/album.controller.ts
index 7c2b39943..4216ca331 100644
--- a/server/src/immich/controllers/album.controller.ts
+++ b/server/src/immich/controllers/album.controller.ts
@@ -31,23 +31,31 @@ export class AlbumController {
}
@Get()
- getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto) {
+ getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto): Promise {
return this.service.getAll(authUser, query);
}
@Post()
- createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto) {
+ createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise {
return this.service.create(authUser, dto);
}
@SharedLinkRoute()
@Get(':id')
- getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Query() dto: AlbumInfoDto) {
+ getAlbumInfo(
+ @AuthUser() authUser: AuthUserDto,
+ @Param() { id }: UUIDParamDto,
+ @Query() dto: AlbumInfoDto,
+ ): Promise {
return this.service.get(authUser, id, dto);
}
@Patch(':id')
- updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto) {
+ updateAlbumInfo(
+ @AuthUser() authUser: AuthUserDto,
+ @Param() { id }: UUIDParamDto,
+ @Body() dto: UpdateDto,
+ ): Promise {
return this.service.update(authUser, id, dto);
}
diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts
index 105760e50..3a652c2e5 100644
--- a/server/src/immich/controllers/asset.controller.ts
+++ b/server/src/immich/controllers/asset.controller.ts
@@ -38,6 +38,7 @@ import {
StreamableFile,
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
+import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation, asStreamableFile } from '../app.utils';
import { Route } from '../interceptors';
@@ -100,6 +101,14 @@ export class AssetController {
return this.service.downloadFile(authUser, id).then(asStreamableFile);
}
+ /**
+ * Get all asset of a device that are in the database, ID only.
+ */
+ @Get('/device/:deviceId')
+ getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
+ return this.service.getUserAssetsByDeviceId(authUser, deviceId);
+ }
+
@Get('statistics')
getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise {
return this.service.getStatistics(authUser, dto);
diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts
index ffa454cd5..b3de4b26c 100644
--- a/server/src/immich/controllers/search.controller.ts
+++ b/server/src/immich/controllers/search.controller.ts
@@ -19,11 +19,6 @@ import { UseValidation } from '../app.utils';
export class SearchController {
constructor(private service: SearchService) {}
- @Get('person')
- searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise {
- return this.service.searchPerson(authUser, dto);
- }
-
@Get()
search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise {
return this.service.search(authUser, dto);
@@ -33,4 +28,9 @@ export class SearchController {
getExploreData(@AuthUser() authUser: AuthUserDto): Promise {
return this.service.getExploreData(authUser) as Promise;
}
+
+ @Get('person')
+ searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise {
+ return this.service.searchPerson(authUser, dto);
+ }
}
diff --git a/server/src/infra/entities/asset-face.entity.ts b/server/src/infra/entities/asset-face.entity.ts
index 66f5c2fd1..c47074d2e 100644
--- a/server/src/infra/entities/asset-face.entity.ts
+++ b/server/src/infra/entities/asset-face.entity.ts
@@ -1,8 +1,9 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AssetEntity } from './asset.entity';
import { PersonEntity } from './person.entity';
@Entity('asset_faces')
+@Index(['personId', 'assetId'])
export class AssetFaceEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts
index 93050b23c..b1f254da4 100644
--- a/server/src/infra/entities/asset.entity.ts
+++ b/server/src/infra/entities/asset.entity.ts
@@ -33,6 +33,7 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
@Index('IDX_day_of_month', { synchronize: false })
@Index('IDX_month', { synchronize: false })
@Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId'])
+@Index(['stackParentId'])
// For all assets, each originalpath must be unique per user and library
export class AssetEntity {
@PrimaryGeneratedColumn('uuid')
diff --git a/server/src/infra/entities/geodata-admin1.entity.ts b/server/src/infra/entities/geodata-admin1.entity.ts
new file mode 100644
index 000000000..36cf0a805
--- /dev/null
+++ b/server/src/infra/entities/geodata-admin1.entity.ts
@@ -0,0 +1,10 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('geodata_admin1')
+export class GeodataAdmin1Entity {
+ @PrimaryColumn({ type: 'varchar' })
+ key!: string;
+
+ @Column({ type: 'varchar' })
+ name!: string;
+}
diff --git a/server/src/infra/entities/geodata-admin2.entity.ts b/server/src/infra/entities/geodata-admin2.entity.ts
new file mode 100644
index 000000000..bd03e8377
--- /dev/null
+++ b/server/src/infra/entities/geodata-admin2.entity.ts
@@ -0,0 +1,10 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('geodata_admin2')
+export class GeodataAdmin2Entity {
+ @PrimaryColumn({ type: 'varchar' })
+ key!: string;
+
+ @Column({ type: 'varchar' })
+ name!: string;
+}
diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/infra/entities/geodata-places.entity.ts
new file mode 100644
index 000000000..244e4261b
--- /dev/null
+++ b/server/src/infra/entities/geodata-places.entity.ts
@@ -0,0 +1,59 @@
+import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity';
+import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity';
+import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
+
+@Entity('geodata_places', { synchronize: false })
+export class GeodataPlacesEntity {
+ @PrimaryColumn({ type: 'integer' })
+ id!: number;
+
+ @Column({ type: 'varchar', length: 200 })
+ name!: string;
+
+ @Column({ type: 'float' })
+ longitude!: number;
+
+ @Column({ type: 'float' })
+ latitude!: number;
+
+ // @Column({
+ // generatedType: 'STORED',
+ // asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)',
+ // type: 'earth',
+ // })
+ earthCoord!: unknown;
+
+ @Column({ type: 'char', length: 2 })
+ countryCode!: string;
+
+ @Column({ type: 'varchar', length: 20, nullable: true })
+ admin1Code!: string;
+
+ @Column({ type: 'varchar', length: 80, nullable: true })
+ admin2Code!: string;
+
+ @Column({
+ type: 'varchar',
+ generatedType: 'STORED',
+ asExpression: `"countryCode" || '.' || "admin1Code"`,
+ nullable: true,
+ })
+ admin1Key!: string;
+
+ @ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
+ admin1!: GeodataAdmin1Entity;
+
+ @Column({
+ type: 'varchar',
+ generatedType: 'STORED',
+ asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`,
+ nullable: true,
+ })
+ admin2Key!: string;
+
+ @ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
+ admin2!: GeodataAdmin2Entity;
+
+ @Column({ type: 'date' })
+ modificationDate!: Date;
+}
diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts
index e4b5c38b4..6c662a20a 100644
--- a/server/src/infra/entities/index.ts
+++ b/server/src/infra/entities/index.ts
@@ -1,3 +1,4 @@
+import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity';
import { ActivityEntity } from './activity.entity';
import { AlbumEntity } from './album.entity';
import { APIKeyEntity } from './api-key.entity';
@@ -6,6 +7,8 @@ import { AssetJobStatusEntity } from './asset-job-status.entity';
import { AssetEntity } from './asset.entity';
import { AuditEntity } from './audit.entity';
import { ExifEntity } from './exif.entity';
+import { GeodataAdmin1Entity } from './geodata-admin1.entity';
+import { GeodataPlacesEntity } from './geodata-places.entity';
import { LibraryEntity } from './library.entity';
import { MoveEntity } from './move.entity';
import { PartnerEntity } from './partner.entity';
@@ -13,6 +16,7 @@ import { PersonEntity } from './person.entity';
import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity';
import { SystemConfigEntity } from './system-config.entity';
+import { SystemMetadataEntity } from './system-metadata.entity';
import { TagEntity } from './tag.entity';
import { UserTokenEntity } from './user-token.entity';
import { UserEntity } from './user.entity';
@@ -25,6 +29,9 @@ export * from './asset-job-status.entity';
export * from './asset.entity';
export * from './audit.entity';
export * from './exif.entity';
+export * from './geodata-admin1.entity';
+export * from './geodata-admin2.entity';
+export * from './geodata-places.entity';
export * from './library.entity';
export * from './move.entity';
export * from './partner.entity';
@@ -32,6 +39,7 @@ export * from './person.entity';
export * from './shared-link.entity';
export * from './smart-info.entity';
export * from './system-config.entity';
+export * from './system-metadata.entity';
export * from './tag.entity';
export * from './user-token.entity';
export * from './user.entity';
@@ -45,12 +53,16 @@ export const databaseEntities = [
AssetJobStatusEntity,
AuditEntity,
ExifEntity,
+ GeodataPlacesEntity,
+ GeodataAdmin1Entity,
+ GeodataAdmin2Entity,
MoveEntity,
PartnerEntity,
PersonEntity,
SharedLinkEntity,
SmartInfoEntity,
SystemConfigEntity,
+ SystemMetadataEntity,
TagEntity,
UserEntity,
UserTokenEntity,
diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts
index 84e72e638..f6c14e1a7 100644
--- a/server/src/infra/entities/system-config.entity.ts
+++ b/server/src/infra/entities/system-config.entity.ts
@@ -66,7 +66,6 @@ export enum SystemConfigKey {
MAP_DARK_STYLE = 'map.darkStyle',
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
- REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
@@ -145,13 +144,6 @@ export enum Colorspace {
P3 = 'p3',
}
-export enum CitiesFile {
- CITIES_15000 = 'cities15000',
- CITIES_5000 = 'cities5000',
- CITIES_1000 = 'cities1000',
- CITIES_500 = 'cities500',
-}
-
export interface SystemConfig {
ffmpeg: {
crf: number;
@@ -200,7 +192,6 @@ export interface SystemConfig {
};
reverseGeocoding: {
enabled: boolean;
- citiesFileOverride: CitiesFile;
};
oauth: {
enabled: boolean;
diff --git a/server/src/infra/entities/system-metadata.entity.ts b/server/src/infra/entities/system-metadata.entity.ts
new file mode 100644
index 000000000..623806db7
--- /dev/null
+++ b/server/src/infra/entities/system-metadata.entity.ts
@@ -0,0 +1,18 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('system_metadata')
+export class SystemMetadataEntity {
+ @PrimaryColumn()
+ key!: string;
+
+ @Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
+ value!: { [key: string]: unknown };
+}
+
+export enum SystemMetadataKey {
+ REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
+}
+
+export interface SystemMetadata extends Record {
+ [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
+}
diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts
index 90477d8ca..7f2423032 100644
--- a/server/src/infra/infra.config.ts
+++ b/server/src/infra/infra.config.ts
@@ -74,6 +74,3 @@ function parseTypeSenseConfig(): ConfigurationOptions {
}
export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig();
-
-export const REVERSE_GEOCODING_DUMP_DIRECTORY =
- process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/';
diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts
index 276058c0b..e0d5711d6 100644
--- a/server/src/infra/infra.module.ts
+++ b/server/src/infra/infra.module.ts
@@ -21,6 +21,7 @@ import {
ISmartInfoRepository,
IStorageRepository,
ISystemConfigRepository,
+ ISystemMetadataRepository,
ITagRepository,
IUserRepository,
IUserTokenRepository,
@@ -56,6 +57,7 @@ import {
SharedLinkRepository,
SmartInfoRepository,
SystemConfigRepository,
+ SystemMetadataRepository,
TagRepository,
TypesenseRepository,
UserRepository,
@@ -84,6 +86,7 @@ const providers: Provider[] = [
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository },
{ provide: IStorageRepository, useClass: FilesystemProvider },
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
+ { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository },
{ provide: IMediaRepository, useClass: MediaRepository },
{ provide: IUserRepository, useClass: UserRepository },
diff --git a/server/src/infra/migrations/1700345818045-SystemMetadata.ts b/server/src/infra/migrations/1700345818045-SystemMetadata.ts
new file mode 100644
index 000000000..0bd9162db
--- /dev/null
+++ b/server/src/infra/migrations/1700345818045-SystemMetadata.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class SystemMetadata1700345818045 implements MigrationInterface {
+ name = 'SystemMetadata1700345818045'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key"))`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP TABLE "system_metadata"`);
+ }
+
+}
diff --git a/server/src/infra/migrations/1700362016675-Geodata.ts b/server/src/infra/migrations/1700362016675-Geodata.ts
new file mode 100644
index 000000000..1ef562ff7
--- /dev/null
+++ b/server/src/infra/migrations/1700362016675-Geodata.ts
@@ -0,0 +1,29 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Geodata1700362016675 implements MigrationInterface {
+ name = 'Geodata1700362016675'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS cube`)
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS earthdistance`)
+ await queryRunner.query(`CREATE TABLE "geodata_admin2" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key"))`);
+ await queryRunner.query(`CREATE TABLE "geodata_admin1" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key"))`);
+ await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin1Key","\"countryCode\" || '.' || \"admin1Code\""]);
+ await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin2Key","\"countryCode\" || '.' || \"admin1Code\" || '.' || \"admin2Code\""]);
+ await queryRunner.query(`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "admin1Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED, "admin2Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED, "modificationDate" date NOT NULL, CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`ALTER TABLE "geodata_places" ADD "earthCoord" earth GENERATED ALWAYS AS (ll_to_earth(latitude, longitude)) STORED`)
+ await queryRunner.query(`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" USING gist ("earthCoord");`)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord"`);
+ await queryRunner.query(`DROP TABLE "geodata_places"`);
+ await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin2Key","immich","public","geodata_places"]);
+ await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin1Key","immich","public","geodata_places"]);
+ await queryRunner.query(`DROP TABLE "geodata_admin1"`);
+ await queryRunner.query(`DROP TABLE "geodata_admin2"`);
+ await queryRunner.query(`DROP EXTENSION cube`);
+ await queryRunner.query(`DROP EXTENSION earthdistance`);
+ }
+
+}
diff --git a/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts
new file mode 100644
index 000000000..723b22b3d
--- /dev/null
+++ b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddAssetFaceIndicies1700752078178 implements MigrationInterface {
+ name = 'AddAssetFaceIndicies1700752078178'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE INDEX "IDX_bf339a24070dac7e71304ec530" ON "asset_faces" ("personId", "assetId") `);
+ await queryRunner.query(`CREATE INDEX "IDX_b463c8edb01364bf2beba08ef1" ON "assets" ("stackParentId") `);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`);
+ }
+
+}
diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts
index fa5862885..b23c559a6 100644
--- a/server/src/infra/repositories/access.repository.ts
+++ b/server/src/infra/repositories/access.repository.ts
@@ -1,6 +1,6 @@
import { IAccessRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { In, Repository } from 'typeorm';
import {
ActivityEntity,
AlbumEntity,
@@ -62,33 +62,52 @@ export class AccessRepository implements IAccessRepository {
});
},
};
+
library = {
- hasOwnerAccess: (userId: string, libraryId: string): Promise => {
- return this.libraryRepository.exist({
- where: {
- id: libraryId,
- ownerId: userId,
- },
- });
+ checkOwnerAccess: async (userId: string, libraryIds: Set): Promise> => {
+ if (libraryIds.size === 0) {
+ return new Set();
+ }
+
+ return this.libraryRepository
+ .find({
+ select: { id: true },
+ where: {
+ id: In([...libraryIds]),
+ ownerId: userId,
+ },
+ })
+ .then((libraries) => new Set(libraries.map((library) => library.id)));
},
- hasPartnerAccess: (userId: string, partnerId: string): Promise => {
- return this.partnerRepository.exist({
- where: {
- sharedWithId: userId,
- sharedById: partnerId,
- },
- });
+
+ checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => {
+ if (partnerIds.size === 0) {
+ return new Set();
+ }
+
+ return this.partnerRepository
+ .createQueryBuilder('partner')
+ .select('partner.sharedById')
+ .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
+ .andWhere('partner.sharedWithId = :userId', { userId })
+ .getMany()
+ .then((partners) => new Set(partners.map((partner) => partner.sharedById)));
},
};
timeline = {
- hasPartnerAccess: (userId: string, partnerId: string): Promise => {
- return this.partnerRepository.exist({
- where: {
- sharedWithId: userId,
- sharedById: partnerId,
- },
- });
+ checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => {
+ if (partnerIds.size === 0) {
+ return new Set();
+ }
+
+ return this.partnerRepository
+ .createQueryBuilder('partner')
+ .select('partner.sharedById')
+ .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] })
+ .andWhere('partner.sharedWithId = :userId', { userId })
+ .getMany()
+ .then((partners) => new Set(partners.map((partner) => partner.sharedById)));
},
};
@@ -198,66 +217,109 @@ export class AccessRepository implements IAccessRepository {
};
authDevice = {
- hasOwnerAccess: (userId: string, deviceId: string): Promise => {
- return this.tokenRepository.exist({
- where: {
- userId,
- id: deviceId,
- },
- });
+ checkOwnerAccess: async (userId: string, deviceIds: Set): Promise> => {
+ if (deviceIds.size === 0) {
+ return new Set();
+ }
+
+ return this.tokenRepository
+ .find({
+ select: { id: true },
+ where: {
+ userId,
+ id: In([...deviceIds]),
+ },
+ })
+ .then((tokens) => new Set(tokens.map((token) => token.id)));
},
};
album = {
- hasOwnerAccess: (userId: string, albumId: string): Promise => {
- return this.albumRepository.exist({
- where: {
- id: albumId,
- ownerId: userId,
- },
- });
- },
+ checkOwnerAccess: async (userId: string, albumIds: Set): Promise> => {
+ if (albumIds.size === 0) {
+ return new Set();
+ }
- hasSharedAlbumAccess: (userId: string, albumId: string): Promise => {
- return this.albumRepository.exist({
- where: {
- id: albumId,
- sharedUsers: {
- id: userId,
+ return this.albumRepository
+ .find({
+ select: { id: true },
+ where: {
+ id: In([...albumIds]),
+ ownerId: userId,
},
- },
- });
+ })
+ .then((albums) => new Set(albums.map((album) => album.id)));
},
- hasSharedLinkAccess: (sharedLinkId: string, albumId: string): Promise