Compare commits
82 commits
renovate/n
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
e1739ac4fc | ||
|
8736c77f7a | ||
|
338a028185 | ||
|
e2d0e944eb | ||
|
f53b70571b | ||
|
2814de4420 | ||
|
024fe1141b | ||
|
086a957a2b | ||
|
84c5b08c25 | ||
|
7e8488694d | ||
|
231b89c9c0 | ||
|
d5f6584e1d | ||
|
7702560b12 | ||
|
982183600d | ||
|
933c24ea6f | ||
|
05e9697dff | ||
|
259700c45f | ||
|
22d79850f6 | ||
|
56aed8246d | ||
|
ca1be71bca | ||
|
6111bf157e | ||
|
2195730fa6 | ||
|
1dc832d392 | ||
|
1a63d3837e | ||
|
bdbaa166d9 | ||
|
812e67d55d | ||
|
dfd6846deb | ||
|
6d3421a505 | ||
|
ede9de146a | ||
|
6959cf689b | ||
|
36ba48b8ae | ||
|
8a2b36ad55 | ||
|
6000c7f3bc | ||
|
5aa658de59 | ||
|
6673f1eb24 | ||
|
a02e91169d | ||
|
ec92608024 | ||
|
5a50d32748 | ||
|
cbdcbd3ab4 | ||
|
cb00d45e3d | ||
|
2b2b1bba63 | ||
|
031420bc78 | ||
|
387faa13d5 | ||
|
6979d43650 | ||
|
af8bb132d0 | ||
|
40964187bb | ||
|
fe3d951f26 | ||
|
6e365b37db | ||
|
5e55a17b2a | ||
|
ffecfbe075 | ||
|
644e52b153 | ||
|
b396e0eee3 | ||
|
8b6a79ad9e | ||
|
696900228b | ||
|
e5d083fe79 | ||
|
d4b3fb942f | ||
|
527d602a9f | ||
|
513f252a0c | ||
|
0fe704c6f9 | ||
|
5a2fc20b20 | ||
|
2a45ad147c | ||
|
fa3f2237eb | ||
|
6aa356e69f | ||
|
a04360f625 | ||
|
48c9e66ae5 | ||
|
05ca555b6e | ||
|
2bb75b6aa9 | ||
|
869d400617 | ||
|
6ae7a92e03 | ||
|
a67f57c0e0 | ||
|
b04cd4edee | ||
|
cd1b6e6976 | ||
|
1fa5e220a1 | ||
|
b21b7f0721 | ||
|
21ed8d5c79 | ||
|
6ac4e98d4b | ||
|
b0db8ed6c4 | ||
|
6522707b49 | ||
|
9483c456d4 | ||
|
5781ae9d82 | ||
|
4d727708e2 | ||
|
5c1c174db1 |
308 changed files with 12578 additions and 1922 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -17,3 +17,5 @@ web/src/api/open-api/**/*.md -diff -merge
|
||||||
web/src/api/open-api/**/*.md linguist-generated=true
|
web/src/api/open-api/**/*.md linguist-generated=true
|
||||||
web/src/api/open-api/**/*.ts -diff -merge
|
web/src/api/open-api/**/*.ts -diff -merge
|
||||||
web/src/api/open-api/**/*.ts linguist-generated=true
|
web/src/api/open-api/**/*.ts linguist-generated=true
|
||||||
|
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
2
.github/workflows/build-mobile.yml
vendored
2
.github/workflows/build-mobile.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.get-ref.outputs.ref }}
|
ref: ${{ steps.get-ref.outputs.ref }}
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
java-version: "12.x"
|
java-version: "12.x"
|
||||||
|
|
3
.github/workflows/cache-cleanup.yml
vendored
3
.github/workflows/cache-cleanup.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Clean up actions cache on PR close
|
name: Cache Cleanup
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
|
@ -10,6 +10,7 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
|
name: Cleanup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
|
|
3
.github/workflows/cli-release.yml
vendored
3
.github/workflows/cli-release.yml
vendored
|
@ -1,9 +1,10 @@
|
||||||
name: Publish Package to npmjs
|
name: CLI Release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
|
name: Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
2
.github/workflows/docker-cleanup.yml
vendored
2
.github/workflows/docker-cleanup.yml
vendored
|
@ -5,7 +5,7 @@
|
||||||
#
|
#
|
||||||
# This workflow will not trigger runs on forked repos.
|
# This workflow will not trigger runs on forked repos.
|
||||||
|
|
||||||
name: Cleanup Old Docker Images
|
name: Docker Cleanup
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Build and Push Docker Images
|
name: Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -18,6 +18,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push:
|
build_and_push:
|
||||||
|
name: Build and Push
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
# Prevent a failure in one image from stopping the other builds
|
# Prevent a failure in one image from stopping the other builds
|
||||||
|
|
47
.github/workflows/test.yml
vendored
47
.github/workflows/test.yml
vendored
|
@ -11,7 +11,7 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: Run end-to-end test suites
|
name: Server (e2e)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
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
|
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:
|
doc-tests:
|
||||||
name: Run documentation checks
|
name: Docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -45,8 +45,12 @@ jobs:
|
||||||
run: npm run check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Run server unit test suites and checks
|
name: Server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -76,7 +80,7 @@ jobs:
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Run cli test suites
|
name: CLI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -106,7 +110,7 @@ jobs:
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Run web unit test suites and checks
|
name: Web
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -140,7 +144,7 @@ jobs:
|
||||||
# if: ${{ !cancelled() }}
|
# if: ${{ !cancelled() }}
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Run mobile unit tests
|
name: Mobile
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -154,7 +158,7 @@ jobs:
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
|
|
||||||
ml-unit-tests:
|
ml-unit-tests:
|
||||||
name: Run ML unit tests and checks
|
name: Machine Learning
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
@ -184,7 +188,7 @@ jobs:
|
||||||
poetry run pytest --cov app
|
poetry run pytest --cov app
|
||||||
|
|
||||||
generated-api-up-to-date:
|
generated-api-up-to-date:
|
||||||
name: Check generated files are up-to-date
|
name: OpenAPI Clients
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -205,11 +209,11 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
generated-typeorm-migrations-up-to-date:
|
generated-typeorm-migrations-up-to-date:
|
||||||
name: Check generated TypeORM migrations are up-to-date
|
name: TypeORM Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres@sha256:6dfee32131933ab4ca25a00360c3f427fdc134de56f9a90c6c9a4956b48aea85
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
@ -232,7 +236,7 @@ jobs:
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build the
|
- name: Build the app
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
|
@ -248,13 +252,30 @@ jobs:
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/infra/migrations/
|
server/src/infra/migrations/
|
||||||
- name: Verify files have not changed
|
- name: Verify migration files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated files not up to date!"
|
echo "ERROR: Generated migration files not up to date!"
|
||||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
- name: Run SQL generation
|
||||||
|
run: npm run sql:generate
|
||||||
|
|
||||||
|
- name: Find file changes
|
||||||
|
uses: tj-actions/verify-changed-files@v13.1
|
||||||
|
id: verify-changed-sql-files
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
server/src/infra/sql
|
||||||
|
|
||||||
|
- name: Verify SQL files have not changed
|
||||||
|
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||||
|
run: |
|
||||||
|
echo "ERROR: Generated SQL files not up to date!"
|
||||||
|
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
# mobile-integration-tests:
|
# mobile-integration-tests:
|
||||||
# name: Run mobile end-to-end integration tests
|
# name: Run mobile end-to-end integration tests
|
||||||
# runs-on: macos-latest
|
# runs-on: macos-latest
|
||||||
|
|
5
Makefile
5
Makefile
|
@ -26,7 +26,10 @@ prod-scale:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
api:
|
api:
|
||||||
cd ./server && npm run api:generate
|
npm --prefix server run api:generate
|
||||||
|
|
||||||
|
sql:
|
||||||
|
npm --prefix server run sql:generate
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
|
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup Lösung für Fotos und Videos</h3>
|
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://immich.app">
|
<a href="https://immich.app">
|
||||||
<img src="design/immich-screenshots.png" title="Haupt-Screenshot">
|
<img src="design/immich-screenshots.png" title="Haupt-Screenshot">
|
||||||
|
@ -32,10 +32,10 @@
|
||||||
|
|
||||||
## Warnung
|
## Warnung
|
||||||
|
|
||||||
- ⚠️ Das Projekt befindet sich unter **sehr aktiver** Entwicklung.
|
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
|
||||||
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
|
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
|
||||||
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
|
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
|
||||||
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup Regel für deine wertvollen Fotos und Videos!
|
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
|
||||||
|
|
||||||
## Inhalt
|
## Inhalt
|
||||||
|
|
||||||
|
@ -54,9 +54,9 @@ Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://im
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
Die Web-Demo kannst du unter https://demo.immich.app finden.
|
Die Web-Demo kannst Du unter https://demo.immich.app finden.
|
||||||
|
|
||||||
Für die Handy-App kannst du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
|
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
|
||||||
|
|
||||||
```bash title="Demo Credential"
|
```bash title="Demo Credential"
|
||||||
Die Anmeldedaten
|
Die Anmeldedaten
|
||||||
|
@ -73,8 +73,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
| Funktionen | Mobil | Web |
|
| Funktionen | Mobil | Web |
|
||||||
| ---------------------------------------------------- | ------ | ----- |
|
| ---------------------------------------------------- | ------ | ----- |
|
||||||
| Fotos & Videos hochladen und ansehen | Ja | Ja |
|
| Fotos & Videos hochladen und ansehen | Ja | Ja |
|
||||||
| Automatisches Backup wenn die App offen ist | Ja | n. a. |
|
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. |
|
||||||
| Selektive Auswahl von Alben zum Backup | Ja | n. a. |
|
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. |
|
||||||
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
|
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
|
||||||
| Unterstützt mehrere Benutzer | Ja | Ja |
|
| Unterstützt mehrere Benutzer | Ja | Ja |
|
||||||
| Album und geteilte Alben | Ja | Ja |
|
| Album und geteilte Alben | Ja | Ja |
|
||||||
|
@ -82,7 +82,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
| Unterstützt RAW Formate | Ja | Ja |
|
| Unterstützt RAW Formate | Ja | Ja |
|
||||||
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
|
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
|
||||||
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
|
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
|
||||||
| Administrative Funktionen (Nutzerverwaltung) | Nein | Ja |
|
| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja |
|
||||||
| Backup im Hintergrund | Ja | n. a. |
|
| Backup im Hintergrund | Ja | n. a. |
|
||||||
| Virtuelles Scrollen | Ja | Ja |
|
| Virtuelles Scrollen | Ja | Ja |
|
||||||
| OAuth Unterstützung | Ja | Ja |
|
| OAuth Unterstützung | Ja | Ja |
|
||||||
|
@ -101,22 +101,22 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
|
|
||||||
## Unterstütze das Projekt
|
## Unterstütze das Projekt
|
||||||
|
|
||||||
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung mir zusätzliche Motivation zu geben um weiterzumachen.
|
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung, um mir zusätzliche Motivation zu geben, weiterzumachen.
|
||||||
|
|
||||||
Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
|
Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
|
||||||
|
|
||||||
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
|
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach, das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
|
||||||
|
|
||||||
### Spenden
|
### Spenden
|
||||||
|
|
||||||
- [Monatliche Spende](https://github.com/sponsors/alextran1502) via GitHub Sponsors
|
- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors
|
||||||
- [Einmalige Spende](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
|
- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors
|
||||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||||
|
|
||||||
## Unterstützer
|
## Mitwirkende
|
||||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
608
cli/package-lock.json
generated
608
cli/package-lock.json
generated
|
@ -29,8 +29,8 @@
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.43.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
|
@ -1604,9 +1604,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.10.0",
|
"version": "20.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
|
||||||
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
|
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
@ -1646,32 +1646,33 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
|
||||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
"integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "6.13.1",
|
||||||
"@typescript-eslint/type-utils": "5.62.0",
|
"@typescript-eslint/type-utils": "6.13.1",
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
"@typescript-eslint/utils": "6.13.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.13.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.4",
|
||||||
"natural-compare-lite": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.5.4",
|
||||||
"tsutils": "^3.21.0"
|
"ts-api-utils": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||||
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
|
@ -1679,26 +1680,126 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
|
||||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
"integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/types": "6.13.1",
|
||||||
"@typescript-eslint/types": "5.62.0",
|
"@typescript-eslint/visitor-keys": "6.13.1"
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
|
||||||
"debug": "^4.3.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"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": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"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": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
|
@ -1706,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": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "5.62.0",
|
"version": "5.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
|
||||||
|
@ -1724,25 +1899,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
|
||||||
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
|
"integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
"@typescript-eslint/typescript-estree": "6.13.1",
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
"@typescript-eslint/utils": "6.13.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"tsutils": "^3.21.0"
|
"ts-api-utils": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "*"
|
"eslint": "^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
|
@ -1750,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": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "5.62.0",
|
"version": "5.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
|
||||||
|
@ -4864,12 +5138,6 @@
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
@ -6023,6 +6291,18 @@
|
||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/ts-jest": {
|
||||||
"version": "29.1.1",
|
"version": "29.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
||||||
|
@ -6170,16 +6450,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
|
@ -7710,9 +7990,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "20.10.0",
|
"version": "20.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
|
||||||
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
|
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
@ -7752,33 +8032,136 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
|
||||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
"integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "6.13.1",
|
||||||
"@typescript-eslint/type-utils": "5.62.0",
|
"@typescript-eslint/type-utils": "6.13.1",
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
"@typescript-eslint/utils": "6.13.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.13.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.4",
|
||||||
"natural-compare-lite": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.5.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/parser": {
|
"@typescript-eslint/parser": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
|
||||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
"integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "6.13.1",
|
||||||
"@typescript-eslint/types": "5.62.0",
|
"@typescript-eslint/types": "6.13.1",
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
"@typescript-eslint/typescript-estree": "6.13.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "6.13.1",
|
||||||
"debug": "^4.3.4"
|
"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": {
|
"@typescript-eslint/scope-manager": {
|
||||||
|
@ -7792,15 +8175,73 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils": {
|
"@typescript-eslint/type-utils": {
|
||||||
"version": "5.62.0",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
|
||||||
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
|
"integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
"@typescript-eslint/typescript-estree": "6.13.1",
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
"@typescript-eslint/utils": "6.13.1",
|
||||||
"debug": "^4.3.4",
|
"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": {
|
"@typescript-eslint/types": {
|
||||||
|
@ -10035,12 +10476,6 @@
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||||
"dev": true
|
"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": {
|
"node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
@ -10865,6 +11300,13 @@
|
||||||
"is-number": "^7.0.0"
|
"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": {
|
"ts-jest": {
|
||||||
"version": "29.1.1",
|
"version": "29.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
|
||||||
|
@ -10947,9 +11389,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"undici-types": {
|
"undici-types": {
|
||||||
|
|
|
@ -29,8 +29,8 @@
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.43.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --project tsconfig.build.json",
|
"build": "tsc --project tsconfig.build.json",
|
||||||
|
|
629
cli/src/api/open-api/api.ts
generated
629
cli/src/api/open-api/api.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.88.2
|
* The version of the OpenAPI document: 1.89.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
@ -447,6 +447,12 @@ export interface AssetBulkDeleteDto {
|
||||||
* @interface AssetBulkUpdateDto
|
* @interface AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
export interface AssetBulkUpdateDto {
|
export interface AssetBulkUpdateDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<string>}
|
* @type {Array<string>}
|
||||||
|
@ -465,6 +471,18 @@ export interface AssetBulkUpdateDto {
|
||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -568,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = {
|
||||||
|
|
||||||
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
export interface AssetFaceResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxX1': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxX2': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxY1': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxY2': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'imageHeight': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'imageWidth': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {PersonResponseDto}
|
||||||
|
* @memberof AssetFaceResponseDto
|
||||||
|
*/
|
||||||
|
'person': PersonResponseDto | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetFaceUpdateDto
|
||||||
|
*/
|
||||||
|
export interface AssetFaceUpdateDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<AssetFaceUpdateItem>}
|
||||||
|
* @memberof AssetFaceUpdateDto
|
||||||
|
*/
|
||||||
|
'data': Array<AssetFaceUpdateItem>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetFaceUpdateItem
|
||||||
|
*/
|
||||||
|
export interface AssetFaceUpdateItem {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetFaceUpdateItem
|
||||||
|
*/
|
||||||
|
'assetId': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetFaceUpdateItem
|
||||||
|
*/
|
||||||
|
'personId': string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
export interface AssetFaceWithoutPersonResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxX1': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxX2': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxY1': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'boundingBoxY2': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'imageHeight': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetFaceWithoutPersonResponseDto
|
||||||
|
*/
|
||||||
|
'imageWidth': number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -824,10 +978,10 @@ export interface AssetResponseDto {
|
||||||
'ownerId': string;
|
'ownerId': string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Array<PersonResponseDto>}
|
* @type {Array<PersonWithFacesResponseDto>}
|
||||||
* @memberof AssetResponseDto
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'people'?: Array<PersonResponseDto>;
|
'people'?: Array<PersonWithFacesResponseDto>;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -1654,6 +1808,19 @@ export interface ExifResponseDto {
|
||||||
*/
|
*/
|
||||||
'timeZone'?: string | null;
|
'timeZone'?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface FaceDto
|
||||||
|
*/
|
||||||
|
export interface FaceDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof FaceDto
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -1767,7 +1934,8 @@ export const JobCommand = {
|
||||||
Start: 'start',
|
Start: 'start',
|
||||||
Pause: 'pause',
|
Pause: 'pause',
|
||||||
Resume: 'resume',
|
Resume: 'resume',
|
||||||
Empty: 'empty'
|
Empty: 'empty',
|
||||||
|
ClearFailed: 'clear-failed'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
||||||
|
@ -2545,6 +2713,49 @@ export interface PersonUpdateDto {
|
||||||
*/
|
*/
|
||||||
'name'?: string;
|
'name'?: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
export interface PersonWithFacesResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'birthDate': string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<AssetFaceWithoutPersonResponseDto>}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'faces': Array<AssetFaceWithoutPersonResponseDto>;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'isHidden': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'name': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof PersonWithFacesResponseDto
|
||||||
|
*/
|
||||||
|
'thumbnailPath': string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -4137,6 +4348,12 @@ export interface UpdateAlbumDto {
|
||||||
* @interface UpdateAssetDto
|
* @interface UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
export interface UpdateAssetDto {
|
export interface UpdateAssetDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'dateTimeOriginal'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -4155,6 +4372,18 @@ export interface UpdateAssetDto {
|
||||||
* @memberof UpdateAssetDto
|
* @memberof UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
'isFavorite'?: boolean;
|
'isFavorite'?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'latitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'longitude'?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -11312,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FaceApi - axios parameter creator
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const FaceApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'id' is not null or undefined
|
||||||
|
assertParamExists('getFaces', 'id', id)
|
||||||
|
const localVarPath = `/face`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (id !== undefined) {
|
||||||
|
localVarQueryParameter['id'] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {FaceDto} faceDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'id' is not null or undefined
|
||||||
|
assertParamExists('reassignFacesById', 'id', id)
|
||||||
|
// verify required parameter 'faceDto' is not null or undefined
|
||||||
|
assertParamExists('reassignFacesById', 'faceDto', faceDto)
|
||||||
|
const localVarPath = `/face/{id}`
|
||||||
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FaceApi - functional programming interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const FaceApiFp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetFaceResponseDto>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {FaceDto} faceDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FaceApi - factory interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = FaceApiFp(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetFaceResponseDto>> {
|
||||||
|
return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||||
|
return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for getFaces operation in FaceApi.
|
||||||
|
* @export
|
||||||
|
* @interface FaceApiGetFacesRequest
|
||||||
|
*/
|
||||||
|
export interface FaceApiGetFacesRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof FaceApiGetFaces
|
||||||
|
*/
|
||||||
|
readonly id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for reassignFacesById operation in FaceApi.
|
||||||
|
* @export
|
||||||
|
* @interface FaceApiReassignFacesByIdRequest
|
||||||
|
*/
|
||||||
|
export interface FaceApiReassignFacesByIdRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof FaceApiReassignFacesById
|
||||||
|
*/
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {FaceDto}
|
||||||
|
* @memberof FaceApiReassignFacesById
|
||||||
|
*/
|
||||||
|
readonly faceDto: FaceDto
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FaceApi - object-oriented interface
|
||||||
|
* @export
|
||||||
|
* @class FaceApi
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
export class FaceApi extends BaseAPI {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof FaceApi
|
||||||
|
*/
|
||||||
|
public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) {
|
||||||
|
return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof FaceApi
|
||||||
|
*/
|
||||||
|
public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) {
|
||||||
|
return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JobApi - axios parameter creator
|
* JobApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
@ -13143,6 +13599,44 @@ export class PartnerApi extends BaseAPI {
|
||||||
*/
|
*/
|
||||||
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
|
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
createPerson: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
const localVarPath = `/person`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} [withHidden]
|
* @param {boolean} [withHidden]
|
||||||
|
@ -13402,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'id' is not null or undefined
|
||||||
|
assertParamExists('reassignFaces', 'id', id)
|
||||||
|
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
|
||||||
|
assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
|
||||||
|
const localVarPath = `/person/{id}/reassign`
|
||||||
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication cookie required
|
||||||
|
|
||||||
|
// authentication api_key required
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||||
|
@ -13504,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
||||||
export const PersonApiFp = function(configuration?: Configuration) {
|
export const PersonApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
|
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} [withHidden]
|
* @param {boolean} [withHidden]
|
||||||
|
@ -13565,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||||
|
@ -13596,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
||||||
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
const localVarFp = PersonApiFp(configuration)
|
const localVarFp = PersonApiFp(configuration)
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
createPerson(options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||||
|
return localVarFp.createPerson(options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||||
|
@ -13650,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
||||||
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||||
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||||
|
return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||||
|
@ -13762,6 +14341,27 @@ export interface PersonApiMergePersonRequest {
|
||||||
readonly mergePersonDto: MergePersonDto
|
readonly mergePersonDto: MergePersonDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for reassignFaces operation in PersonApi.
|
||||||
|
* @export
|
||||||
|
* @interface PersonApiReassignFacesRequest
|
||||||
|
*/
|
||||||
|
export interface PersonApiReassignFacesRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof PersonApiReassignFaces
|
||||||
|
*/
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {AssetFaceUpdateDto}
|
||||||
|
* @memberof PersonApiReassignFaces
|
||||||
|
*/
|
||||||
|
readonly assetFaceUpdateDto: AssetFaceUpdateDto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for updatePeople operation in PersonApi.
|
* Request parameters for updatePeople operation in PersonApi.
|
||||||
* @export
|
* @export
|
||||||
|
@ -13804,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest {
|
||||||
* @extends {BaseAPI}
|
* @extends {BaseAPI}
|
||||||
*/
|
*/
|
||||||
export class PersonApi extends BaseAPI {
|
export class PersonApi extends BaseAPI {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof PersonApi
|
||||||
|
*/
|
||||||
|
public createPerson(options?: AxiosRequestConfig) {
|
||||||
|
return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||||
|
@ -13870,6 +14480,17 @@ export class PersonApi extends BaseAPI {
|
||||||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof PersonApi
|
||||||
|
*/
|
||||||
|
public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) {
|
||||||
|
return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||||
|
|
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.88.2
|
* The version of the OpenAPI document: 1.89.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.88.2
|
* The version of the OpenAPI document: 1.89.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.88.2
|
* The version of the OpenAPI document: 1.89.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
|
@ -4,7 +4,7 @@
|
||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.88.2
|
* The version of the OpenAPI document: 1.89.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|
|
@ -59,7 +59,7 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: npm run dev --host
|
command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
|
@ -108,11 +108,11 @@ services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -65,12 +65,12 @@ services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -23,7 +23,7 @@ services:
|
||||||
- database
|
- database
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||||
command: -c fsync=off
|
command: -c fsync=off
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
|
@ -69,12 +69,12 @@ services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|
58
docs/docs/guides/remote-access.md
Normal file
58
docs/docs/guides/remote-access.md
Normal file
|
@ -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.
|
|
@ -144,7 +144,7 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
|
||||||
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`.
|
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`.
|
||||||
|
|
||||||
```bash title="Start the containers using docker compose command"
|
```bash title="Start the containers using docker compose command"
|
||||||
docker-compose up -d # or `docker compose up -d` based on your docker-compose version
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
@ -162,7 +162,7 @@ If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired
|
||||||
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||||
|
|
||||||
```bash title="Upgrade Immich"
|
```bash title="Upgrade Immich"
|
||||||
docker-compose pull && docker-compose up -d # Or `docker compose up -d`
|
docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
:::caution Automatic Updates
|
:::caution Automatic Updates
|
||||||
|
|
120
docs/package-lock.json
generated
120
docs/package-lock.json
generated
|
@ -15,7 +15,7 @@
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^2.0.0",
|
||||||
"docusaurus-lunr-search": "^2.3.2",
|
"docusaurus-lunr-search": "^2.3.2",
|
||||||
"docusaurus-preset-openapi": "^0.6.3",
|
"docusaurus-preset-openapi": "^0.6.3",
|
||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.25",
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -2603,6 +2603,14 @@
|
||||||
"react-dom": "^16.8.4 || ^17.0.0"
|
"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": {
|
"node_modules/@docusaurus/theme-common": {
|
||||||
"version": "2.4.3",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz",
|
"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"
|
"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": {
|
"node_modules/@docusaurus/theme-search-algolia": {
|
||||||
"version": "2.4.3",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz",
|
"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"
|
"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": {
|
"node_modules/@docusaurus/theme-translations": {
|
||||||
"version": "2.4.3",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
|
||||||
|
@ -4948,9 +4972,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "1.2.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
@ -5995,6 +6019,14 @@
|
||||||
"react-dom": "^16.8.4 || ^17"
|
"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": {
|
"node_modules/docusaurus-plugin-openapi": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz",
|
"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"
|
"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": {
|
"node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"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"
|
"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": {
|
"node_modules/dom-converter": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||||
|
@ -10801,15 +10849,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "2.8.8",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin-prettier.js"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
@ -16745,6 +16793,13 @@
|
||||||
"rtlcss": "^3.5.0",
|
"rtlcss": "^3.5.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"utility-types": "^3.10.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": {
|
"@docusaurus/theme-common": {
|
||||||
|
@ -16768,6 +16823,13 @@
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"use-sync-external-store": "^1.2.0",
|
"use-sync-external-store": "^1.2.0",
|
||||||
"utility-types": "^3.10.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": {
|
"@docusaurus/theme-search-algolia": {
|
||||||
|
@ -16791,6 +16853,13 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"utility-types": "^3.10.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": {
|
"@docusaurus/theme-translations": {
|
||||||
|
@ -18515,9 +18584,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clsx": {
|
"clsx": {
|
||||||
"version": "1.2.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
|
||||||
},
|
},
|
||||||
"collapse-white-space": {
|
"collapse-white-space": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
|
@ -19243,6 +19312,13 @@
|
||||||
"to-vfile": "^6.1.0",
|
"to-vfile": "^6.1.0",
|
||||||
"unified": "^9.0.0",
|
"unified": "^9.0.0",
|
||||||
"unist-util-is": "^4.0.2"
|
"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": {
|
"docusaurus-plugin-openapi": {
|
||||||
|
@ -19268,6 +19344,11 @@
|
||||||
"webpack": "^5.73.0"
|
"webpack": "^5.73.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
@ -19321,6 +19402,13 @@
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"webpack": "^5.73.0"
|
"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": {
|
"dom-converter": {
|
||||||
|
@ -22663,9 +22751,9 @@
|
||||||
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "2.8.8",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"pretty-error": {
|
"pretty-error": {
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^2.0.0",
|
||||||
"docusaurus-lunr-search": "^2.3.2",
|
"docusaurus-lunr-search": "^2.3.2",
|
||||||
"docusaurus-preset-openapi": "^0.6.3",
|
"docusaurus-preset-openapi": "^0.6.3",
|
||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.25",
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||||
"@tsconfig/docusaurus": "^1.0.5",
|
"@tsconfig/docusaurus": "^1.0.5",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|
|
@ -61,8 +61,12 @@
|
||||||
.searchbox__input {
|
.searchbox__input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
-webkit-transition: box-shadow 0.4s ease, background 0.4s ease;
|
-webkit-transition:
|
||||||
transition: box-shadow 0.4s ease, background 0.4s ease;
|
box-shadow 0.4s ease,
|
||||||
|
background 0.4s ease;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.4s ease,
|
||||||
|
background 0.4s ease;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: inset 0 0 0 1px #cccccc;
|
box-shadow: inset 0 0 0 1px #cccccc;
|
||||||
|
@ -243,7 +247,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.algolia-autocomplete .ds-dropdown-menu {
|
.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) {
|
@media (min-width: 601px) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.11-bookworm as builder
|
FROM python:3.11-bookworm@sha256:ba7a7ac30c38e119c4304f98ef0e188f90f4f67a958bb6899da9defb99bfb471 as builder
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
|
@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||||
COPY poetry.lock pyproject.toml ./
|
COPY poetry.lock pyproject.toml ./
|
||||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm
|
FROM python:3.11-slim-bookworm@sha256:cc758519481092eb5a4a5ab0c1b303e288880d59afc601958d19e95b300bc86b
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mambaorg/micromamba:bookworm-slim as builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:e296d47be09fc5d260eba9b191f60496f028a4f3ec41e8a14d48c0bae2c60244 as builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
TRANSFORMERS_CACHE=/cache \
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.88.2"
|
version = "1.89.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -49,7 +49,6 @@ dart_code_metrics:
|
||||||
# Common
|
# Common
|
||||||
- avoid-accessing-collections-by-constant-index
|
- avoid-accessing-collections-by-constant-index
|
||||||
- avoid-accessing-other-classes-private-members
|
- avoid-accessing-other-classes-private-members
|
||||||
- avoid-async-call-in-sync-function
|
|
||||||
- avoid-cascade-after-if-null
|
- avoid-cascade-after-if-null
|
||||||
- avoid-collapsible-if
|
- avoid-collapsible-if
|
||||||
- avoid-collection-methods-with-unrelated-types
|
- avoid-collection-methods-with-unrelated-types
|
||||||
|
|
|
@ -35,8 +35,8 @@ platform :android do
|
||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 112,
|
"android.injected.version.code" => 113,
|
||||||
"android.injected.version.name" => "1.88.2",
|
"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')
|
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')
|
||||||
|
|
|
@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||||
distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2
|
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
|
|
@ -144,6 +144,8 @@
|
||||||
"control_bottom_app_bar_stack": "Stack",
|
"control_bottom_app_bar_stack": "Stack",
|
||||||
"control_bottom_app_bar_unarchive": "Unarchive",
|
"control_bottom_app_bar_unarchive": "Unarchive",
|
||||||
"control_bottom_app_bar_upload": "Upload",
|
"control_bottom_app_bar_upload": "Upload",
|
||||||
|
"control_bottom_app_bar_edit_time": "Edit Date & Time",
|
||||||
|
"control_bottom_app_bar_edit_location": "Edit Location",
|
||||||
"create_album_page_untitled": "Untitled",
|
"create_album_page_untitled": "Untitled",
|
||||||
"create_shared_album_page_create": "Create",
|
"create_shared_album_page_create": "Create",
|
||||||
"create_shared_album_page_share": "Share",
|
"create_shared_album_page_share": "Share",
|
||||||
|
@ -165,6 +167,7 @@
|
||||||
"exif_bottom_sheet_description": "Add Description...",
|
"exif_bottom_sheet_description": "Add Description...",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
@ -461,5 +464,18 @@
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "Un-Stack",
|
"viewer_unstack": "Un-Stack",
|
||||||
"scaffold_body_error_occured": "Error occured"
|
"scaffold_body_error_occurred": "Error occurred",
|
||||||
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
|
"action_common_cancel": "Cancel",
|
||||||
|
"action_common_update": "Update",
|
||||||
|
"edit_location_dialog_title": "Location",
|
||||||
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
|
"location_picker_choose_on_map": "Choose on map",
|
||||||
|
"location_picker_latitude": "Latitude",
|
||||||
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
|
"location_picker_latitude_error": "Enter a valid latitude",
|
||||||
|
"location_picker_longitude": "Longitude",
|
||||||
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
|
"location_picker_longitude_error": "Enter a valid longitude"
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ class ImmichTestHelper {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [dbProvider.overrideWithValue(db)],
|
overrides: [dbProvider.overrideWithValue(db)],
|
||||||
child: app.getMainWidget(),
|
child: const app.MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Post run tasks
|
// Post run tasks
|
||||||
|
|
|
@ -19,7 +19,7 @@ platform :ios do
|
||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.88.2"
|
version_number: "1.89.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
Color immichBackgroundColor = const Color(0xFFf6f8fe);
|
const Color immichBackgroundColor = Color(0xFFf6f8fe);
|
||||||
Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
|
const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0);
|
||||||
Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);
|
const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250);
|
||||||
|
|
36
mobile/lib/extensions/asset_extensions.dart
Normal file
36
mobile/lib/extensions/asset_extensions.dart
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:timezone/timezone.dart';
|
||||||
|
|
||||||
|
extension TZExtension on Asset {
|
||||||
|
/// Returns the created time of the asset from the exif info (if available) or from
|
||||||
|
/// the fileCreatedAt field, adjusted to the timezone value from the exif info along with
|
||||||
|
/// the timezone offset in [Duration]
|
||||||
|
(DateTime, Duration) getTZAdjustedTimeAndOffset() {
|
||||||
|
DateTime dt = fileCreatedAt.toLocal();
|
||||||
|
if (exifInfo?.dateTimeOriginal != null) {
|
||||||
|
dt = exifInfo!.dateTimeOriginal!;
|
||||||
|
if (exifInfo?.timeZone != null) {
|
||||||
|
dt = dt.toUtc();
|
||||||
|
try {
|
||||||
|
final location = getLocation(exifInfo!.timeZone!);
|
||||||
|
dt = TZDateTime.from(dt, location);
|
||||||
|
} on LocationNotFoundException {
|
||||||
|
RegExp re = RegExp(
|
||||||
|
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
final m = re.firstMatch(exifInfo!.timeZone!);
|
||||||
|
if (m != null) {
|
||||||
|
final duration = Duration(
|
||||||
|
hours: int.parse(m.group(1) ?? '0'),
|
||||||
|
minutes: int.parse(m.group(2) ?? '0'),
|
||||||
|
);
|
||||||
|
dt = dt.add(duration);
|
||||||
|
return (dt, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (dt, dt.timeZoneOffset);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,22 +4,32 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
extension ScaffoldBody<T> on AsyncValue<T> {
|
extension LogOnError<T> on AsyncValue<T> {
|
||||||
static final Logger _scaffoldBodyLog = Logger("ScaffoldBody");
|
static final Logger _asyncErrorLogger = Logger("AsyncValue");
|
||||||
|
|
||||||
Widget scaffoldBodyWhen({
|
Widget widgetWhen({
|
||||||
|
bool skipLoadingOnRefresh = true,
|
||||||
|
Widget Function()? onLoading,
|
||||||
|
Widget Function(Object? error, StackTrace? stack)? onError,
|
||||||
required Widget Function(T data) onData,
|
required Widget Function(T data) onData,
|
||||||
Widget? onError,
|
|
||||||
}) {
|
}) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const Center(
|
bool skip = false;
|
||||||
child: ImmichLoadingIndicator(),
|
if (isRefreshing) {
|
||||||
);
|
skip = skipLoadingOnRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skip) {
|
||||||
|
return onLoading?.call() ??
|
||||||
|
const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError && !hasValue) {
|
if (hasError && !hasValue) {
|
||||||
_scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace);
|
_asyncErrorLogger.severe("Error occured", error, stackTrace);
|
||||||
return onError ?? const ScaffoldErrorBody();
|
return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
return onData(requireValue);
|
return onData(requireValue);
|
||||||
|
|
|
@ -45,7 +45,7 @@ extension ContextHelper on BuildContext {
|
||||||
) =>
|
) =>
|
||||||
AutoRouter.of(this).navigate(route);
|
AutoRouter.of(this).navigate(route);
|
||||||
|
|
||||||
// Auto-Push replace route from the current context
|
// Auto-Push replace route from the current context
|
||||||
Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
|
Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
|
||||||
AutoRouter.of(this).replace(route);
|
AutoRouter.of(this).replace(route);
|
||||||
|
|
||||||
|
|
4
mobile/lib/extensions/duration_extensions.dart
Normal file
4
mobile/lib/extensions/duration_extensions.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
extension TZOffsetExtension on Duration {
|
||||||
|
String formatAsOffset() =>
|
||||||
|
"${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
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/services.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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:timezone/data/latest.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/background.service.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/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/immich_logger.service.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/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/http_ssl_cert_override.dart';
|
||||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
@ -43,10 +44,11 @@ void main() async {
|
||||||
await initApp();
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(db);
|
||||||
HttpOverrides.global = HttpSSLCertOverride();
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [dbProvider.overrideWithValue(db)],
|
overrides: [dbProvider.overrideWithValue(db)],
|
||||||
child: getMainWidget(),
|
child: const MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -108,16 +110,6 @@ Future<Isar> loadDb() async {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getMainWidget() {
|
|
||||||
return EasyLocalization(
|
|
||||||
supportedLocales: locales,
|
|
||||||
path: translationsPath,
|
|
||||||
useFallbackTranslations: true,
|
|
||||||
fallbackLocale: locales.first,
|
|
||||||
child: const ImmichApp(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
const ImmichApp({super.key});
|
const ImmichApp({super.key});
|
||||||
|
|
||||||
|
@ -167,10 +159,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
// Android 8 does not support transparent app bars
|
// Android 8 does not support transparent app bars
|
||||||
final info = await DeviceInfoPlugin().androidInfo;
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
if (info.version.sdkInt <= 26) {
|
if (info.version.sdkInt <= 26) {
|
||||||
overlayStyle =
|
overlayStyle = context.isDarkTheme
|
||||||
MediaQuery.of(context).platformBrightness == Brightness.light
|
? SystemUiOverlayStyle.dark
|
||||||
? SystemUiOverlayStyle.light
|
: SystemUiOverlayStyle.light;
|
||||||
: SystemUiOverlayStyle.dark;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
|
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
|
||||||
|
@ -202,22 +193,33 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
locale: context.locale,
|
locale: context.locale,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: Stack(
|
home: MaterialApp.router(
|
||||||
children: [
|
title: 'Immich',
|
||||||
MaterialApp.router(
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'Immich',
|
themeMode: ref.watch(immichThemeProvider),
|
||||||
debugShowCheckedModeBanner: false,
|
darkTheme: immichDarkTheme,
|
||||||
themeMode: ref.watch(immichThemeProvider),
|
theme: immichLightTheme,
|
||||||
darkTheme: immichDarkTheme,
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
theme: immichLightTheme,
|
routerDelegate: router.delegate(
|
||||||
routeInformationParser: router.defaultRouteParser(),
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||||
routerDelegate: router.delegate(
|
),
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const ImmichLoadingOverlay(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -95,7 +95,11 @@ class ActivityStatisticsNotifier extends StateNotifier<int> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchStatistics() async {
|
Future<void> fetchStatistics() async {
|
||||||
state = await _activityService.getStatistics(albumId, assetId: assetId);
|
final count =
|
||||||
|
await _activityService.getStatistics(albumId, assetId: assetId);
|
||||||
|
if (mounted) {
|
||||||
|
state = count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addActivity() async {
|
Future<void> addActivity() async {
|
||||||
|
|
|
@ -4,12 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/activities/models/activity.model.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/modules/activities/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/ui/confirm_dialog.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/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 30,
|
height: 30,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: CachedNetworkImageProvider(
|
image: CachedNetworkImageProvider(
|
||||||
getThumbnailUrlForRemoteId(
|
getThumbnailUrlForRemoteId(
|
||||||
|
@ -231,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(appBarTitle)),
|
appBar: AppBar(title: Text(appBarTitle)),
|
||||||
body: activities.maybeWhen(
|
body: activities.widgetWhen(
|
||||||
orElse: () {
|
onData: (data) {
|
||||||
return const Center(child: ImmichLoadingIndicator());
|
|
||||||
},
|
|
||||||
data: (data) {
|
|
||||||
final liked = data.firstWhereOrNull(
|
final liked = data.firstWhereOrNull(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.type == ActivityType.like &&
|
a.type == ActivityType.like &&
|
||||||
|
|
|
@ -65,7 +65,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
|
|
@ -68,46 +68,46 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: album.thumbnail.value == null
|
child: album.thumbnail.value == null
|
||||||
? buildEmptyThumbnail()
|
? buildEmptyThumbnail()
|
||||||
: buildAlbumThumbnail(),
|
: buildAlbumThumbnail(),
|
||||||
),
|
),
|
||||||
Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.only(
|
child: Padding(
|
||||||
left: 8.0,
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
right: 8.0,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
album.name,
|
||||||
Text(
|
overflow: TextOverflow.ellipsis,
|
||||||
album.name,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
album.assetCount == 1
|
||||||
album.assetCount == 1
|
? 'album_thumbnail_card_item'
|
||||||
? 'album_thumbnail_card_item'
|
: 'album_thumbnail_card_items',
|
||||||
: 'album_thumbnail_card_items',
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
).tr(args: ['${album.assetCount}']),
|
|
||||||
if (album.shared)
|
|
||||||
const Text(
|
|
||||||
'album_thumbnail_card_shared',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(args: ['${album.assetCount}']),
|
||||||
],
|
if (album.shared)
|
||||||
),
|
const Text(
|
||||||
],
|
'album_thumbnail_card_shared',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
final isProcessing = useProcessingOverlay();
|
||||||
final comments = album.shared
|
final comments = album.shared
|
||||||
? ref.watch(
|
? ref.watch(
|
||||||
activityStatisticsStateProvider(
|
activityStatisticsStateProvider(
|
||||||
|
@ -52,7 +53,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
deleteAlbum() async {
|
deleteAlbum() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
final bool success;
|
final bool success;
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
|
@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showConfirmationDialog() async {
|
Future<void> showConfirmationDialog() async {
|
||||||
|
@ -89,7 +90,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, 'Cancel'),
|
onPressed: () => context.pop('Cancel'),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Cancel',
|
'Cancel',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -100,7 +101,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, 'Confirm');
|
context.pop('Confirm');
|
||||||
deleteAlbum();
|
deleteAlbum();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -122,7 +123,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
void onLeaveAlbumPressed() async {
|
void onLeaveAlbumPressed() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||||
|
@ -131,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
context
|
context
|
||||||
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
|
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "album_viewer_appbar_share_err_leave".tr(),
|
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 {
|
void onRemoveFromAlbumPressed() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
||||||
|
@ -153,12 +154,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
selectionDisabled();
|
selectionDisabled();
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "album_viewer_appbar_share_err_remove".tr(),
|
msg: "album_viewer_appbar_share_err_remove".tr(),
|
||||||
|
@ -167,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleShareAssets(
|
void handleShareAssets(
|
||||||
|
@ -198,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
void onShareAssetsTo() async {
|
void onShareAssetsTo() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
handleShareAssets(ref, context, selected);
|
handleShareAssets(ref, context, selected);
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildBottomSheetActions() {
|
buildBottomSheetActions() {
|
||||||
|
@ -253,7 +254,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_add_alt_rounded),
|
leading: const Icon(Icons.person_add_alt_rounded),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
onAddUsers!(album);
|
onAddUsers!(album);
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -265,7 +266,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
leading: const Icon(Icons.share_rounded),
|
leading: const Icon(Icons.share_rounded),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
|
context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"control_bottom_app_bar_share",
|
"control_bottom_app_bar_share",
|
||||||
|
@ -286,7 +287,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.add_photo_alternate_outlined),
|
leading: const Icon(Icons.add_photo_alternate_outlined),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
onAddPhotos!(album);
|
onAddPhotos!(album);
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
|
|
@ -24,10 +24,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
final owner = album.owner.value;
|
final owner = album.owner.value;
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
final activityEnabled = useState(album.activityEnabled);
|
final activityEnabled = useState(album.activityEnabled);
|
||||||
|
final isProcessing = useProcessingOverlay();
|
||||||
final isOwner = owner?.id == userId;
|
final isOwner = owner?.id == userId;
|
||||||
|
|
||||||
void showErrorMessage() {
|
void showErrorMessage() {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "shared_album_section_people_action_error".tr(),
|
msg: "shared_album_section_people_action_error".tr(),
|
||||||
|
@ -37,7 +38,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
void leaveAlbum() async {
|
void leaveAlbum() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final isSuccess =
|
final isSuccess =
|
||||||
|
@ -54,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
showErrorMessage();
|
showErrorMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeUserFromAlbum(User user) async {
|
void removeUserFromAlbum(User user) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
|
@ -70,8 +71,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
showErrorMessage();
|
showErrorMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleUserClick(User user) {
|
void handleUserClick(User user) {
|
||||||
|
@ -180,9 +181,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
onPressed: () {
|
onPressed: () => context.autoPop(null),
|
||||||
context.autoPop(null);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text("translated_text_options".tr()),
|
title: Text("translated_text_options".tr()),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.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';
|
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/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.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 userId = ref.watch(authenticationProvider).userId;
|
||||||
final selection = useState<Set<Asset>>({});
|
final selection = useState<Set<Asset>>({});
|
||||||
final multiSelectEnabled = useState(false);
|
final multiSelectEnabled = useState(false);
|
||||||
|
final isProcessing = useProcessingOverlay();
|
||||||
|
|
||||||
useEffect(
|
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
|
// Check if there is new assets add
|
||||||
if (returnPayload.selectedAssets.isNotEmpty) {
|
isProcessing.value = true;
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
|
||||||
|
|
||||||
var addAssetsResult =
|
var addAssetsResult =
|
||||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||||
returnPayload.selectedAssets,
|
returnPayload.selectedAssets,
|
||||||
albumInfo,
|
albumInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addAssetsResult != null &&
|
if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) {
|
||||||
addAssetsResult.successfullyAdded > 0) {
|
ref.invalidate(albumDetailProvider(albumId));
|
||||||
ref.invalidate(albumDetailProvider(albumId));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +100,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
isProcessing.value = true;
|
||||||
|
|
||||||
var isSuccess = await ref
|
var isSuccess = await ref
|
||||||
.watch(albumServiceProvider)
|
.watch(albumServiceProvider)
|
||||||
|
@ -112,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,13 +258,11 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||||
loading: () => AppBar(),
|
loading: () => AppBar(),
|
||||||
),
|
),
|
||||||
body: album.when(
|
body: album.widgetWhen(
|
||||||
data: (data) => WillPopScope(
|
onData: (data) => WillPopScope(
|
||||||
onWillPop: onWillPop,
|
onWillPop: onWillPop,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () => titleFocusNode.unfocus(),
|
||||||
titleFocusNode.unfocus();
|
|
||||||
},
|
|
||||||
child: ImmichAssetGrid(
|
child: ImmichAssetGrid(
|
||||||
renderList: data.renderList,
|
renderList: data.renderList,
|
||||||
listener: selectionListener,
|
listener: selectionListener,
|
||||||
|
@ -285,10 +281,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.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';
|
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||||
|
@ -85,12 +86,8 @@ class AssetSelectionPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: renderList.when(
|
body: renderList.widgetWhen(
|
||||||
data: (data) => buildBody(data),
|
onData: (data) => buildBody(data),
|
||||||
error: (error, stackTrace) => Center(
|
|
||||||
child: Text(error.toString()),
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,49 +135,56 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCreateAlbumButton() {
|
Widget buildCreateAlbumButton() {
|
||||||
return GestureDetector(
|
return LayoutBuilder(
|
||||||
onTap: () {
|
builder: (context, constraints) {
|
||||||
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
|
var cardSize = constraints.maxWidth;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: cardSize,
|
||||||
|
height: cardSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: isDarkTheme
|
||||||
|
? const Color.fromARGB(255, 53, 53, 53)
|
||||||
|
: const Color.fromARGB(255, 203, 203, 203),
|
||||||
|
),
|
||||||
|
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 16,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'library_page_new_album',
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 32),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: isDarkTheme
|
|
||||||
? const Color.fromARGB(255, 53, 53, 53)
|
|
||||||
: const Color.fromARGB(255, 203, 203, 203),
|
|
||||||
),
|
|
||||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.add_rounded,
|
|
||||||
size: 28,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 8.0,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'library_page_new_album',
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.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/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.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';
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
|
||||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
|
@ -137,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: suggestedShareUsers.when(
|
body: suggestedShareUsers.widgetWhen(
|
||||||
data: (users) {
|
onData: (users) {
|
||||||
for (var sharedUsers in album.sharedUsers) {
|
for (var sharedUsers in album.sharedUsers) {
|
||||||
users.removeWhere(
|
users.removeWhere(
|
||||||
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
|
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
|
||||||
|
@ -147,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return buildUserList(users);
|
return buildUserList(users);
|
||||||
},
|
},
|
||||||
error: (e, _) => Text("Error loading suggested users $e"),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.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/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.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';
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
|
||||||
class SelectUserForSharingPage extends HookConsumerWidget {
|
class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
|
@ -42,7 +42,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
|
|
||||||
ScaffoldMessenger(
|
ScaffoldMessenger(
|
||||||
child: SnackBar(
|
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(
|
body: suggestedShareUsers.widgetWhen(
|
||||||
data: (users) {
|
onData: (users) {
|
||||||
return buildUserList(users);
|
return buildUserList(users);
|
||||||
},
|
},
|
||||||
error: (e, _) => Text("Error loading suggested users $e"),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.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';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 64,
|
height: 64,
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: ListTile(
|
||||||
children: [
|
shape: const RoundedRectangleBorder(
|
||||||
ListTile(
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(10),
|
leading: const Icon(
|
||||||
),
|
Icons.unarchive_rounded,
|
||||||
leading: const Icon(
|
),
|
||||||
Icons.unarchive_rounded,
|
title: Text(
|
||||||
),
|
'control_bottom_app_bar_unarchive'.tr(),
|
||||||
title: Text(
|
style: const TextStyle(fontSize: 14),
|
||||||
'control_bottom_app_bar_unarchive'.tr(),
|
),
|
||||||
style: const TextStyle(fontSize: 14),
|
onTap: processing.value
|
||||||
),
|
? null
|
||||||
onTap: processing.value
|
: () async {
|
||||||
? null
|
processing.value = true;
|
||||||
: () async {
|
try {
|
||||||
processing.value = true;
|
await handleArchiveAssets(
|
||||||
try {
|
ref,
|
||||||
await handleArchiveAssets(
|
context,
|
||||||
ref,
|
selection.value.toList(),
|
||||||
context,
|
shouldArchive: false,
|
||||||
selection.value.toList(),
|
);
|
||||||
shouldArchive: false,
|
} finally {
|
||||||
);
|
processing.value = false;
|
||||||
} finally {
|
selectionEnabledHook.value = false;
|
||||||
processing.value = false;
|
}
|
||||||
selectionEnabledHook.value = false;
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return archivedAssets.when(
|
return Scaffold(
|
||||||
loading: () => Scaffold(
|
appBar: archivedAssets.maybeWhen(
|
||||||
appBar: buildAppBar("?"),
|
data: (data) => buildAppBar(data.totalAssets.toString()),
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
orElse: () => buildAppBar("?"),
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => Scaffold(
|
body: archivedAssets.widgetWhen(
|
||||||
appBar: buildAppBar("Error"),
|
onData: (data) => data.isEmpty
|
||||||
body: Center(child: Text(error.toString())),
|
|
||||||
),
|
|
||||||
data: (data) => Scaffold(
|
|
||||||
appBar: buildAppBar(data.totalAssets.toString()),
|
|
||||||
body: data.isEmpty
|
|
||||||
? Center(
|
? Center(
|
||||||
child: Text('archive_page_no_archived_assets'.tr()),
|
child: Text('archive_page_no_archived_assets'.tr()),
|
||||||
)
|
)
|
||||||
|
|
|
@ -62,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget {
|
||||||
ClipboardData(text: assetDetail.toString()),
|
ClipboardData(text: assetDetail.toString()),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text("Copied to clipboard"),
|
content: Text(
|
||||||
|
"Copied to clipboard",
|
||||||
|
style: context.textTheme.bodyLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,14 +4,15 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:timezone/timezone.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
|
||||||
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||||
|
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
@ -21,111 +22,84 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
|
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
bool hasCoordinates(ExifInfo? exifInfo) =>
|
|
||||||
exifInfo != null &&
|
|
||||||
exifInfo.latitude != null &&
|
|
||||||
exifInfo.longitude != null &&
|
|
||||||
exifInfo.latitude != 0 &&
|
|
||||||
exifInfo.longitude != 0;
|
|
||||||
|
|
||||||
String formatTimeZone(Duration d) =>
|
|
||||||
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
|
||||||
|
|
||||||
String get formattedDateTime {
|
|
||||||
DateTime dt = asset.fileCreatedAt.toLocal();
|
|
||||||
String? timeZone;
|
|
||||||
if (asset.exifInfo?.dateTimeOriginal != null) {
|
|
||||||
dt = asset.exifInfo!.dateTimeOriginal!;
|
|
||||||
if (asset.exifInfo?.timeZone != null) {
|
|
||||||
dt = dt.toUtc();
|
|
||||||
try {
|
|
||||||
final location = getLocation(asset.exifInfo!.timeZone!);
|
|
||||||
dt = TZDateTime.from(dt, location);
|
|
||||||
} on LocationNotFoundException {
|
|
||||||
RegExp re = RegExp(
|
|
||||||
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
|
||||||
caseSensitive: false,
|
|
||||||
);
|
|
||||||
final m = re.firstMatch(asset.exifInfo!.timeZone!);
|
|
||||||
if (m != null) {
|
|
||||||
final duration = Duration(
|
|
||||||
hours: int.parse(m.group(1) ?? '0'),
|
|
||||||
minutes: int.parse(m.group(2) ?? '0'),
|
|
||||||
);
|
|
||||||
dt = dt.add(duration);
|
|
||||||
timeZone = formatTimeZone(duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final date = DateFormat.yMMMEd().format(dt);
|
|
||||||
final time = DateFormat.jm().format(dt);
|
|
||||||
timeZone ??= formatTimeZone(dt.timeZoneOffset);
|
|
||||||
|
|
||||||
return '$date • $time $timeZone';
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
|
|
||||||
if (!hasCoordinates(exifInfo)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final double latitude = exifInfo!.latitude!;
|
|
||||||
final double longitude = exifInfo.longitude!;
|
|
||||||
|
|
||||||
const zoomLevel = 16;
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
Uri uri = Uri(
|
|
||||||
scheme: 'geo',
|
|
||||||
host: '$latitude,$longitude',
|
|
||||||
queryParameters: {
|
|
||||||
'z': '$zoomLevel',
|
|
||||||
'q': '$latitude,$longitude($formattedDateTime)',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
} else if (Platform.isIOS) {
|
|
||||||
var params = {
|
|
||||||
'll': '$latitude,$longitude',
|
|
||||||
'q': formattedDateTime,
|
|
||||||
'z': '$zoomLevel',
|
|
||||||
};
|
|
||||||
Uri uri = Uri.https('maps.apple.com', '/', params);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uri(
|
|
||||||
scheme: 'https',
|
|
||||||
host: 'openstreetmap.org',
|
|
||||||
queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'},
|
|
||||||
fragment: 'map=$zoomLevel/$latitude/$longitude',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||||
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
||||||
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
|
|
||||||
|
bool hasCoordinates() =>
|
||||||
|
exifInfo != null &&
|
||||||
|
exifInfo.latitude != null &&
|
||||||
|
exifInfo.longitude != null &&
|
||||||
|
exifInfo.latitude != 0 &&
|
||||||
|
exifInfo.longitude != 0;
|
||||||
|
|
||||||
|
String formattedDateTime() {
|
||||||
|
final (dt, timeZone) =
|
||||||
|
(assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset();
|
||||||
|
final date = DateFormat.yMMMEd().format(dt);
|
||||||
|
final time = DateFormat.jm().format(dt);
|
||||||
|
|
||||||
|
return '$date • $time GMT${timeZone.formatAsOffset()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri?> createCoordinatesUri() async {
|
||||||
|
if (!hasCoordinates()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double latitude = exifInfo!.latitude!;
|
||||||
|
final double longitude = exifInfo.longitude!;
|
||||||
|
|
||||||
|
const zoomLevel = 16;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
Uri uri = Uri(
|
||||||
|
scheme: 'geo',
|
||||||
|
host: '$latitude,$longitude',
|
||||||
|
queryParameters: {
|
||||||
|
'z': '$zoomLevel',
|
||||||
|
'q': '$latitude,$longitude($formattedDateTime)',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
var params = {
|
||||||
|
'll': '$latitude,$longitude',
|
||||||
|
'q': formattedDateTime,
|
||||||
|
'z': '$zoomLevel',
|
||||||
|
};
|
||||||
|
Uri uri = Uri.https('maps.apple.com', '/', params);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uri(
|
||||||
|
scheme: 'https',
|
||||||
|
host: 'openstreetmap.org',
|
||||||
|
queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'},
|
||||||
|
fragment: 'map=$zoomLevel/$latitude/$longitude',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
buildMap() {
|
buildMap() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return MapThumbnail(
|
return MapThumbnail(
|
||||||
|
showAttribution: false,
|
||||||
coords: LatLng(
|
coords: LatLng(
|
||||||
exifInfo?.latitude ?? 0,
|
exifInfo?.latitude ?? 0,
|
||||||
exifInfo?.longitude ?? 0,
|
exifInfo?.longitude ?? 0,
|
||||||
),
|
),
|
||||||
height: 150,
|
height: 150,
|
||||||
zoom: 16.0,
|
width: constraints.maxWidth,
|
||||||
|
zoom: 12.0,
|
||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
|
@ -139,7 +113,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onTap: (tapPosition, latLong) async {
|
onTap: (tapPosition, latLong) async {
|
||||||
Uri? uri = await _createCoordinatesUri(exifInfo);
|
Uri? uri = await createCoordinatesUri();
|
||||||
|
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -181,8 +155,26 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
|
|
||||||
buildLocation() {
|
buildLocation() {
|
||||||
// Guard no lat/lng
|
// Guard no lat/lng
|
||||||
if (!hasCoordinates(exifInfo)) {
|
if (!hasCoordinates()) {
|
||||||
return Container();
|
return asset.isRemote
|
||||||
|
? ListTile(
|
||||||
|
minLeadingWidth: 0,
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
leading: const Icon(Icons.location_on),
|
||||||
|
title: Text(
|
||||||
|
"exif_bottom_sheet_location_add",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
onTap: () => handleEditLocation(
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
[assetWithExif.value ?? asset],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -191,13 +183,29 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
"exif_bottom_sheet_location",
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: context.textTheme.labelMedium?.copyWith(
|
children: [
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
Text(
|
||||||
fontWeight: FontWeight.w600,
|
"exif_bottom_sheet_location",
|
||||||
),
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
).tr(),
|
color:
|
||||||
|
context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
if (asset.isRemote)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleEditLocation(
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
[assetWithExif.value ?? asset],
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
iconSize: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
buildMap(),
|
buildMap(),
|
||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
|
@ -233,12 +241,27 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDate() {
|
buildDate() {
|
||||||
return Text(
|
return Row(
|
||||||
formattedDateTime,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
Text(
|
||||||
fontSize: 14,
|
formattedDateTime(),
|
||||||
),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (asset.isRemote)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleEditDateTime(
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
[assetWithExif.value ?? asset],
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
iconSize: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +386,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: hasCoordinates(exifInfo) ? 5 : 0,
|
flex: hasCoordinates() ? 5 : 0,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: buildLocation(),
|
child: buildLocation(),
|
||||||
|
@ -402,9 +425,8 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8.0),
|
|
||||||
buildLocation(),
|
buildLocation(),
|
||||||
SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),
|
SizedBox(height: hasCoordinates() ? 16.0 : 6.0),
|
||||||
buildDetail(),
|
buildDetail(),
|
||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
],
|
],
|
||||||
|
|
|
@ -514,7 +514,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
currentAsset,
|
currentAsset,
|
||||||
stackElements.elementAt(stackIndex.value),
|
stackElements.elementAt(stackIndex.value),
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
ctx.pop();
|
||||||
context.autoPop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -541,7 +541,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
stackElements.elementAt(1),
|
stackElements.elementAt(1),
|
||||||
childrenToRemove: [currentAsset],
|
childrenToRemove: [currentAsset],
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
ctx.pop();
|
||||||
context.autoPop();
|
context.autoPop();
|
||||||
} else {
|
} else {
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
|
@ -551,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
removeAssetFromStack();
|
removeAssetFromStack();
|
||||||
Navigator.pop(ctx);
|
ctx.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -569,7 +569,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
currentAsset,
|
currentAsset,
|
||||||
childrenToRemove: stack,
|
childrenToRemove: stack,
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
ctx.pop();
|
||||||
context.autoPop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -795,6 +795,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
tag: isFromDto
|
tag: isFromDto
|
||||||
? '${a.remoteId}-$heroOffset'
|
? '${a.remoteId}-$heroOffset'
|
||||||
: a.id + heroOffset,
|
: a.id + heroOffset,
|
||||||
|
transitionOnUserGestures: true,
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
|
|
|
@ -375,6 +375,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
await _getBackupAlbumsInfo();
|
await _getBackupAlbumsInfo();
|
||||||
await updateServerInfo();
|
await updateServerInfo();
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
|
} else {
|
||||||
|
log.warning("cannot get backup info - background backup is in progress!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
msg.tr(),
|
msg.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 64,
|
height: 64,
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: ListTile(
|
||||||
children: [
|
shape: const RoundedRectangleBorder(
|
||||||
ListTile(
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(10),
|
leading: const Icon(
|
||||||
),
|
Icons.star_border,
|
||||||
leading: const Icon(
|
),
|
||||||
Icons.star_border,
|
title: const Text(
|
||||||
),
|
"Unfavorite",
|
||||||
title: const Text(
|
style: TextStyle(fontSize: 14),
|
||||||
"Unfavorite",
|
),
|
||||||
style: TextStyle(fontSize: 14),
|
onTap: processing.value ? null : unfavorite,
|
||||||
),
|
|
||||||
onTap: processing.value ? null : unfavorite,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: buildAppBar(),
|
appBar: buildAppBar(),
|
||||||
body: ref.watch(favoriteAssetsProvider).when(
|
body: ref.watch(favoriteAssetsProvider).widgetWhen(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
onData: (data) => data.isEmpty
|
||||||
error: (error, stackTrace) => Center(child: Text(error.toString())),
|
|
||||||
data: (data) => data.isEmpty
|
|
||||||
? Center(
|
? Center(
|
||||||
child: Text('favorites_page_no_favorites'.tr()),
|
child: Text('favorites_page_no_favorites'.tr()),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.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/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.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/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
class ImmichAssetGrid extends HookConsumerWidget {
|
class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
|
@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
if (renderList != null) return buildAssetGridView(renderList!);
|
if (renderList != null) return buildAssetGridView(renderList!);
|
||||||
|
|
||||||
final renderListFuture = ref.watch(renderListProvider(assets!));
|
final renderListFuture = ref.watch(renderListProvider(assets!));
|
||||||
return renderListFuture.when(
|
return renderListFuture.widgetWhen(
|
||||||
data: (renderList) => buildAssetGridView(renderList),
|
onData: (renderList) => buildAssetGridView(renderList),
|
||||||
error: (err, stack) => Center(child: Text("$err")),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,9 @@ class ThumbnailImage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.decelerate,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: multiselectEnabled && isSelected
|
border: multiselectEnabled && isSelected
|
||||||
? Border.all(
|
? Border.all(
|
||||||
|
|
|
@ -19,6 +19,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
final void Function() onCreateNewAlbum;
|
final void Function() onCreateNewAlbum;
|
||||||
final void Function() onUpload;
|
final void Function() onUpload;
|
||||||
final void Function() onStack;
|
final void Function() onStack;
|
||||||
|
final void Function() onEditTime;
|
||||||
|
final void Function() onEditLocation;
|
||||||
|
|
||||||
final List<Album> albums;
|
final List<Album> albums;
|
||||||
final List<Album> sharedAlbums;
|
final List<Album> sharedAlbums;
|
||||||
|
@ -37,6 +39,8 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
required this.onCreateNewAlbum,
|
required this.onCreateNewAlbum,
|
||||||
required this.onUpload,
|
required this.onUpload,
|
||||||
required this.onStack,
|
required this.onStack,
|
||||||
|
required this.onEditTime,
|
||||||
|
required this.onEditLocation,
|
||||||
this.selectionAssetState = const SelectionAssetState(),
|
this.selectionAssetState = const SelectionAssetState(),
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -74,6 +78,18 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||||
label: "control_bottom_app_bar_favorite".tr(),
|
label: "control_bottom_app_bar_favorite".tr(),
|
||||||
onPressed: enabled ? onFavorite : null,
|
onPressed: enabled ? onFavorite : null,
|
||||||
),
|
),
|
||||||
|
if (hasRemote)
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.edit_calendar_outlined,
|
||||||
|
label: "control_bottom_app_bar_edit_time".tr(),
|
||||||
|
onPressed: enabled ? onEditTime : null,
|
||||||
|
),
|
||||||
|
if (hasRemote)
|
||||||
|
ControlBoxButton(
|
||||||
|
iconData: Icons.edit_location_alt_outlined,
|
||||||
|
label: "control_bottom_app_bar_edit_location".tr(),
|
||||||
|
onPressed: enabled ? onEditLocation : null,
|
||||||
|
),
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.delete_outline_rounded,
|
iconData: Icons.delete_outline_rounded,
|
||||||
label: "control_bottom_app_bar_delete".tr(),
|
label: "control_bottom_app_bar_delete".tr(),
|
||||||
|
|
|
@ -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_app_bar.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.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/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
|
@ -50,7 +51,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
|
|
||||||
final tipOneOpacity = useState(0.0);
|
final tipOneOpacity = useState(0.0);
|
||||||
final refreshCount = useState(0);
|
final refreshCount = useState(0);
|
||||||
final processing = useState(false);
|
final processing = useProcessingOverlay();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
|
@ -311,6 +312,34 @@ class HomePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onEditTime() async {
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
handleEditDateTime(ref, context, remoteAssets.toList());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEditLocation() async {
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_favorite_err_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
handleEditLocation(ref, context, remoteAssets.toList());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refreshAssets() async {
|
Future<void> refreshAssets() async {
|
||||||
final fullRefresh = refreshCount.value > 0;
|
final fullRefresh = refreshCount.value > 0;
|
||||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||||
|
@ -323,16 +352,12 @@ class HomePage extends HookConsumerWidget {
|
||||||
} else {
|
} else {
|
||||||
refreshCount.value++;
|
refreshCount.value++;
|
||||||
// set counter back to 0 if user does not request refresh again
|
// set counter back to 0 if user does not request refresh again
|
||||||
Timer(const Duration(seconds: 4), () {
|
Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
|
||||||
refreshCount.value = 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLoadingIndicator() {
|
buildLoadingIndicator() {
|
||||||
Timer(const Duration(seconds: 2), () {
|
Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
|
||||||
tipOneOpacity.value = 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -414,8 +439,9 @@ class HomePage extends HookConsumerWidget {
|
||||||
enabled: !processing.value,
|
enabled: !processing.value,
|
||||||
selectionAssetState: selectionAssetState.value,
|
selectionAssetState: selectionAssetState.value,
|
||||||
onStack: onStack,
|
onStack: onStack,
|
||||||
|
onEditTime: onEditTime,
|
||||||
|
onEditLocation: onEditLocation,
|
||||||
),
|
),
|
||||||
if (processing.value) const Center(child: ImmichLoadingIndicator()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
@ -21,6 +23,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
AuthenticationNotifier(
|
AuthenticationNotifier(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
this._db,
|
this._db,
|
||||||
|
this._ref,
|
||||||
) : super(
|
) : super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
|
@ -36,6 +39,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
|
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
|
final StateNotifierProviderRef<AuthenticationNotifier, AuthenticationState>
|
||||||
|
_ref;
|
||||||
final _log = Logger("AuthenticationNotifier");
|
final _log = Logger("AuthenticationNotifier");
|
||||||
|
|
||||||
Future<bool> login(
|
Future<bool> login(
|
||||||
|
@ -111,6 +116,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
Store.delete(StoreKey.currentUser),
|
Store.delete(StoreKey.currentUser),
|
||||||
Store.delete(StoreKey.accessToken),
|
Store.delete(StoreKey.accessToken),
|
||||||
]);
|
]);
|
||||||
|
_ref.invalidate(albumProvider);
|
||||||
|
_ref.invalidate(sharedAlbumProvider);
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
|
@ -222,5 +229,6 @@ final authenticationProvider =
|
||||||
return AuthenticationNotifier(
|
return AuthenticationNotifier(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,7 +48,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
/// Fetch the server login credential and enables oAuth login if necessary
|
/// Fetch the server login credential and enables oAuth login if necessary
|
||||||
/// Returns true if successful, false otherwise
|
/// Returns true if successful, false otherwise
|
||||||
Future<bool> getServerLoginCredential() async {
|
Future<bool> getServerLoginCredential() async {
|
||||||
final serverUrl = serverEndpointController.text.trim();
|
final serverUrl = sanitizeUrl(serverEndpointController.text);
|
||||||
|
|
||||||
// Guard empty URL
|
// Guard empty URL
|
||||||
if (serverUrl.isEmpty) {
|
if (serverUrl.isEmpty) {
|
||||||
|
@ -127,6 +127,12 @@ class LoginForm extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
populateTestLoginInfo() {
|
populateTestLoginInfo() {
|
||||||
|
usernameController.text = 'demo@immich.app';
|
||||||
|
passwordController.text = 'demo';
|
||||||
|
serverEndpointController.text = 'https://demo.immich.app';
|
||||||
|
}
|
||||||
|
|
||||||
|
populateTestLoginInfo1() {
|
||||||
usernameController.text = 'testuser@email.com';
|
usernameController.text = 'testuser@email.com';
|
||||||
passwordController.text = 'password';
|
passwordController.text = 'password';
|
||||||
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||||
|
@ -144,7 +150,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
await ref.read(authenticationProvider.notifier).login(
|
await ref.read(authenticationProvider.notifier).login(
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
serverEndpointController.text.trim(),
|
sanitizeUrl(serverEndpointController.text),
|
||||||
);
|
);
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
|
@ -181,7 +187,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
oAuthServerConfig = await oAuthService
|
oAuthServerConfig = await oAuthService
|
||||||
.getOAuthServerConfig(serverEndpointController.text);
|
.getOAuthServerConfig(sanitizeUrl(serverEndpointController.text));
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -203,7 +209,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
.watch(authenticationProvider.notifier)
|
.watch(authenticationProvider.notifier)
|
||||||
.setSuccessLoginInfo(
|
.setSuccessLoginInfo(
|
||||||
accessToken: loginResponseDto.accessToken,
|
accessToken: loginResponseDto.accessToken,
|
||||||
serverUrl: serverEndpointController.text,
|
serverUrl: sanitizeUrl(serverEndpointController.text),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
@ -299,7 +305,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
serverEndpointController.text,
|
sanitizeUrl(serverEndpointController.text),
|
||||||
style: context.textTheme.displaySmall,
|
style: context.textTheme.displaySmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -387,6 +393,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onDoubleTap: () => populateTestLoginInfo(),
|
onDoubleTap: () => populateTestLoginInfo(),
|
||||||
|
onLongPress: () => populateTestLoginInfo1(),
|
||||||
child: RotationTransition(
|
child: RotationTransition(
|
||||||
turns: logoAnimationController,
|
turns: logoAnimationController,
|
||||||
child: const ImmichLogo(
|
child: const ImmichLogo(
|
||||||
|
|
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
113
mobile/lib/modules/map/ui/map_location_picker.dart
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
class MapLocationPickerPage extends HookConsumerWidget {
|
||||||
|
final LatLng? initialLatLng;
|
||||||
|
|
||||||
|
const MapLocationPickerPage({super.key, this.initialLatLng});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectedLatLng = useState<LatLng>(initialLatLng ?? LatLng(0, 0));
|
||||||
|
final isDarkTheme =
|
||||||
|
ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
|
||||||
|
final isLoading =
|
||||||
|
ref.watch(mapStateNotifier.select((state) => state.isLoading));
|
||||||
|
final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
// Override app theme based on map theme
|
||||||
|
data: isDarkTheme ? immichDarkTheme : immichLightTheme,
|
||||||
|
child: Scaffold(
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
if (!isLoading)
|
||||||
|
FlutterMap(
|
||||||
|
options: MapOptions(
|
||||||
|
maxBounds:
|
||||||
|
LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
|
||||||
|
interactiveFlags: InteractiveFlag.doubleTapZoom |
|
||||||
|
InteractiveFlag.drag |
|
||||||
|
InteractiveFlag.flingAnimation |
|
||||||
|
InteractiveFlag.pinchMove |
|
||||||
|
InteractiveFlag.pinchZoom,
|
||||||
|
center: LatLng(20, 20),
|
||||||
|
zoom: 2,
|
||||||
|
minZoom: 1,
|
||||||
|
maxZoom: maxZoom,
|
||||||
|
onTap: (tapPosition, point) => selectedLatLng.value = point,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
ref.read(mapStateNotifier.notifier).getTileLayer(),
|
||||||
|
MarkerLayer(
|
||||||
|
markers: [
|
||||||
|
Marker(
|
||||||
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
|
point: selectedLatLng.value,
|
||||||
|
builder: (ctx) => const Image(
|
||||||
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isLoading)
|
||||||
|
Positioned(
|
||||||
|
top: context.height * 0.35,
|
||||||
|
left: context.width * 0.425,
|
||||||
|
child: const ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomSheet: BottomSheet(
|
||||||
|
onClosing: () {},
|
||||||
|
builder: (context) => SizedBox(
|
||||||
|
height: 150,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${selectedLatLng.value.latitude.toStringAsFixed(4)}, ${selectedLatLng.value.longitude.toStringAsFixed(4)}",
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.autoPop(selectedLatLng.value),
|
||||||
|
child: const Text("map_location_picker_page_use_location")
|
||||||
|
.tr(),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.autoPop(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
child: const Text("action_common_cancel").tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_map/plugin_api.dart';
|
import 'package:flutter_map/plugin_api.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
@ -12,13 +14,15 @@ class MapThumbnail extends HookConsumerWidget {
|
||||||
final double zoom;
|
final double zoom;
|
||||||
final List<Marker> markers;
|
final List<Marker> markers;
|
||||||
final double height;
|
final double height;
|
||||||
|
final double width;
|
||||||
final bool showAttribution;
|
final bool showAttribution;
|
||||||
final bool isDarkTheme;
|
final bool isDarkTheme;
|
||||||
|
|
||||||
const MapThumbnail({
|
const MapThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
required this.coords,
|
required this.coords,
|
||||||
required this.height,
|
this.height = 100,
|
||||||
|
this.width = 100,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.zoom = 1,
|
this.zoom = 1,
|
||||||
this.showAttribution = true,
|
this.showAttribution = true,
|
||||||
|
@ -28,18 +32,33 @@ class MapThumbnail extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final mapController = useMapController();
|
||||||
|
final isMapReady = useRef(false);
|
||||||
ref.watch(mapStateNotifier.select((s) => s.mapStyle));
|
ref.watch(mapStateNotifier.select((s) => s.mapStyle));
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (isMapReady.value && mapController.center != coords) {
|
||||||
|
mapController.move(coords, zoom);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[coords],
|
||||||
|
);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: height,
|
height: height,
|
||||||
|
width: width,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
|
mapController: mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
interactiveFlags: InteractiveFlag.none,
|
interactiveFlags: InteractiveFlag.none,
|
||||||
center: coords,
|
center: coords,
|
||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
onMapReady: () => isMapReady.value = true,
|
||||||
),
|
),
|
||||||
nonRotatedChildren: [
|
nonRotatedChildren: [
|
||||||
if (showAttribution)
|
if (showAttribution)
|
||||||
|
|
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
32
mobile/lib/modules/map/utils/map_controller_hook.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
|
||||||
|
MapController useMapController({
|
||||||
|
String? debugLabel,
|
||||||
|
List<Object?>? keys,
|
||||||
|
}) {
|
||||||
|
return use(_MapControllerHook(keys: keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapControllerHook extends Hook<MapController> {
|
||||||
|
const _MapControllerHook({List<Object?>? keys}) : super(keys: keys);
|
||||||
|
|
||||||
|
@override
|
||||||
|
HookState<MapController, Hook<MapController>> createState() =>
|
||||||
|
_MapControllerHookState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapControllerHookState
|
||||||
|
extends HookState<MapController, _MapControllerHook> {
|
||||||
|
late final controller = MapController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
MapController build(BuildContext context) => controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() => controller.dispose();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugLabel => 'useMapController';
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
||||||
// in onMapEvent() since MapEventMove#id is not populated properly in the
|
// in onMapEvent() since MapEventMove#id is not populated properly in the
|
||||||
// current version of flutter_map(4.0.0) used
|
// current version of flutter_map(4.0.0) used
|
||||||
bool forceAssetUpdate = false;
|
bool forceAssetUpdate = false;
|
||||||
|
bool isMapReady = false;
|
||||||
late final Debounce debounce;
|
late final Debounce debounce;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -79,7 +80,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
||||||
bool forceReload = false,
|
bool forceReload = false,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
final bounds = mapController.bounds;
|
final bounds = isMapReady ? mapController.bounds : null;
|
||||||
if (bounds != null) {
|
if (bounds != null) {
|
||||||
final oldAssetsInBounds = assetsInBounds.toSet();
|
final oldAssetsInBounds = assetsInBounds.toSet();
|
||||||
assetsInBounds =
|
assetsInBounds =
|
||||||
|
@ -455,6 +456,7 @@ class MapPageState extends ConsumerState<MapPage> {
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
onMapReady: () {
|
onMapReady: () {
|
||||||
|
isMapReady = true;
|
||||||
mapController.mapEventStream.listen(onMapEvent);
|
mapController.mapEventStream.listen(onMapEvent);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -17,7 +17,7 @@ class MemoryLane extends HookConsumerWidget {
|
||||||
.whenData(
|
.whenData(
|
||||||
(memories) => memories != null
|
(memories) => memories != null
|
||||||
? Container(
|
? Container(
|
||||||
margin: const EdgeInsets.only(top: 10),
|
margin: const EdgeInsets.only(top: 10, left: 10),
|
||||||
height: 200,
|
height: 200,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class PartnerDetailPage extends HookConsumerWidget {
|
class PartnerDetailPage extends HookConsumerWidget {
|
||||||
|
@ -71,8 +71,8 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: assets.when(
|
body: assets.widgetWhen(
|
||||||
data: (renderList) => renderList.isEmpty
|
onData: (renderList) => renderList.isEmpty
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -84,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||||
onRefresh: () =>
|
onRefresh: () =>
|
||||||
ref.read(assetProvider.notifier).getPartnerAssets(partner),
|
ref.read(assetProvider.notifier).getPartnerAssets(partner),
|
||||||
),
|
),
|
||||||
error: (e, _) => Text("Error loading partners:\n$e"),
|
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
|
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
@ -34,7 +35,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
for (User u in users)
|
for (User u in users)
|
||||||
SimpleDialogOption(
|
SimpleDialogOption(
|
||||||
onPressed: () => Navigator.pop(context, u),
|
onPressed: () => context.pop(u),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -70,8 +71,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return ConfirmDialog(
|
return ConfirmDialog(
|
||||||
title: "partner_page_stop_sharing_title",
|
title: "partner_page_stop_sharing_title",
|
||||||
content:
|
content: "partner_page_stop_sharing_content".tr(args: [u.name]),
|
||||||
"partner_page_stop_sharing_content".tr(args: [u.name]),
|
|
||||||
onOk: () => ref.read(partnerServiceProvider).removePartner(u),
|
onOk: () => ref.read(partnerServiceProvider).removePartner(u),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -118,6 +118,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
@ -126,12 +127,15 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(fontSize: 14),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
Align(
|
||||||
onPressed: availableUsers.whenOrNull(
|
alignment: Alignment.center,
|
||||||
data: (data) => addNewUsersHandler,
|
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(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,7 +6,7 @@ part of 'person.service.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b';
|
String _$personServiceHash() => r'cde0a9c029d16ddde2adcd58ae8c863bf8cc1fed';
|
||||||
|
|
||||||
/// See also [personService].
|
/// See also [personService].
|
||||||
@ProviderFor(personService)
|
@ProviderFor(personService)
|
||||||
|
|
|
@ -29,9 +29,8 @@ class CuratedPlacesRow extends CuratedRow {
|
||||||
onTap: () => context.autoPush(
|
onTap: () => context.autoPush(
|
||||||
const MapRoute(),
|
const MapRoute(),
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox.square(
|
||||||
height: imageSize,
|
dimension: imageSize,
|
||||||
width: imageSize,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -43,6 +42,7 @@ class CuratedPlacesRow extends CuratedRow {
|
||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
height: imageSize,
|
height: imageSize,
|
||||||
|
width: imageSize,
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
isDarkTheme: context.isDarkTheme,
|
isDarkTheme: context.isDarkTheme,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.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 {
|
class AllMotionPhotosPage extends HookConsumerWidget {
|
||||||
const AllMotionPhotosPage({super.key});
|
const AllMotionPhotosPage({super.key});
|
||||||
|
@ -21,14 +21,10 @@ class AllMotionPhotosPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: motionPhotos.when(
|
body: motionPhotos.widgetWhen(
|
||||||
data: (assets) => ImmichAssetGrid(
|
onData: (assets) => ImmichAssetGrid(
|
||||||
assets: assets,
|
assets: assets,
|
||||||
),
|
),
|
||||||
error: (e, s) => Text(e.toString()),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/people.provider.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/modules/search/ui/explore_grid.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
|
|
||||||
class AllPeoplePage extends HookConsumerWidget {
|
class AllPeoplePage extends HookConsumerWidget {
|
||||||
const AllPeoplePage({super.key});
|
const AllPeoplePage({super.key});
|
||||||
|
@ -23,12 +23,8 @@ class AllPeoplePage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: curatedPeople.when(
|
body: curatedPeople.widgetWhen(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
onData: (people) => ExploreGrid(
|
||||||
error: (err, stack) => Center(
|
|
||||||
child: Text('Error: $err'),
|
|
||||||
),
|
|
||||||
data: (people) => ExploreGrid(
|
|
||||||
isPeople: true,
|
isPeople: true,
|
||||||
curatedContent: people,
|
curatedContent: people,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.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 {
|
class AllVideosPage extends HookConsumerWidget {
|
||||||
const AllVideosPage({super.key});
|
const AllVideosPage({super.key});
|
||||||
|
@ -21,14 +21,10 @@ class AllVideosPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: videos.when(
|
body: videos.widgetWhen(
|
||||||
data: (assets) => ImmichAssetGrid(
|
onData: (assets) => ImmichAssetGrid(
|
||||||
assets: assets,
|
assets: assets,
|
||||||
),
|
),
|
||||||
error: (e, s) => Text(e.toString()),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_content.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/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/explore_grid.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';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class CuratedLocationPage extends HookConsumerWidget {
|
class CuratedLocationPage extends HookConsumerWidget {
|
||||||
|
@ -26,12 +26,8 @@ class CuratedLocationPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: curatedLocation.when(
|
body: curatedLocation.widgetWhen(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
onData: (curatedLocations) => ExploreGrid(
|
||||||
error: (err, stack) => Center(
|
|
||||||
child: Text('Error: $err'),
|
|
||||||
),
|
|
||||||
data: (curatedLocations) => ExploreGrid(
|
|
||||||
curatedContent: curatedLocations
|
curatedContent: curatedLocations
|
||||||
.map(
|
.map(
|
||||||
(l) => CuratedContent(
|
(l) => CuratedContent(
|
||||||
|
|
|
@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'
|
||||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart' as isar_store;
|
import 'package:immich_mobile/shared/models/store.dart' as isar_store;
|
||||||
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
class PersonResultPage extends HookConsumerWidget {
|
class PersonResultPage extends HookConsumerWidget {
|
||||||
|
@ -112,7 +111,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen(
|
body: ref.watch(personAssetsProvider(personId)).widgetWhen(
|
||||||
onData: (renderList) => ImmichAssetGrid(
|
onData: (renderList) => ImmichAssetGrid(
|
||||||
renderList: renderList,
|
renderList: renderList,
|
||||||
topWidget: Padding(
|
topWidget: Padding(
|
||||||
|
@ -137,7 +136,6 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/recently_added.provider.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 {
|
class RecentlyAddedPage extends HookConsumerWidget {
|
||||||
const RecentlyAddedPage({super.key});
|
const RecentlyAddedPage({super.key});
|
||||||
|
@ -21,14 +21,10 @@ class RecentlyAddedPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: recents.when(
|
body: recents.widgetWhen(
|
||||||
data: (searchResponse) => ImmichAssetGrid(
|
onData: (searchResponse) => ImmichAssetGrid(
|
||||||
assets: searchResponse,
|
assets: searchResponse,
|
||||||
),
|
),
|
||||||
error: (e, s) => Text(e.toString()),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_content.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/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/modules/search/ui/search_suggestion_list.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
|
@ -73,10 +74,9 @@ class SearchPage extends HookConsumerWidget {
|
||||||
buildPeople() {
|
buildPeople() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: imageSize,
|
height: imageSize,
|
||||||
child: curatedPeople.when(
|
child: curatedPeople.widgetWhen(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
onData: (people) => CuratedPeopleRow(
|
||||||
data: (people) => CuratedPeopleRow(
|
|
||||||
content: people.take(12).toList(),
|
content: people.take(12).toList(),
|
||||||
onTap: (content, index) {
|
onTap: (content, index) {
|
||||||
context.autoPush(
|
context.autoPush(
|
||||||
|
@ -97,10 +97,9 @@ class SearchPage extends HookConsumerWidget {
|
||||||
buildPlaces() {
|
buildPlaces() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: imageSize,
|
height: imageSize,
|
||||||
child: curatedLocation.when(
|
child: curatedLocation.widgetWhen(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
onData: (locations) => CuratedPlacesRow(
|
||||||
data: (locations) => CuratedPlacesRow(
|
|
||||||
isMapEnabled: isMapEnabled,
|
isMapEnabled: isMapEnabled,
|
||||||
content: locations
|
content: locations
|
||||||
.map(
|
.map(
|
||||||
|
|
|
@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$appSettingsServiceHash() =>
|
String _$appSettingsServiceHash() =>
|
||||||
r'957a65af6967701112f3076b507f9738fec4b7be';
|
r'45ea609a91d250290431a7a08a14d16b37c7515d';
|
||||||
|
|
||||||
/// See also [appSettingsService].
|
/// See also [appSettingsService].
|
||||||
@ProviderFor(appSettingsService)
|
@ProviderFor(appSettingsService)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/asset_list_settings/asset_list_settings.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
|
import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
|
||||||
|
@ -18,9 +19,7 @@ class SettingsPage extends HookConsumerWidget {
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
onPressed: () {
|
onPressed: () => context.pop(),
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
),
|
),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
|
|
@ -46,9 +46,11 @@ class SharedLinkItem extends ConsumerWidget {
|
||||||
} else if (difference.inHours > 0) {
|
} else if (difference.inHours > 0) {
|
||||||
expiresText = "shared_link_expires_hours".plural(difference.inHours);
|
expiresText = "shared_link_expires_hours".plural(difference.inHours);
|
||||||
} else if (difference.inMinutes > 0) {
|
} else if (difference.inMinutes > 0) {
|
||||||
expiresText = "shared_link_expires_minutes".plural(difference.inMinutes);
|
expiresText =
|
||||||
|
"shared_link_expires_minutes".plural(difference.inMinutes);
|
||||||
} else if (difference.inSeconds > 0) {
|
} else if (difference.inSeconds > 0) {
|
||||||
expiresText = "shared_link_expires_seconds".plural(difference.inSeconds);
|
expiresText =
|
||||||
|
"shared_link_expires_seconds".plural(difference.inSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Text(
|
return Text(
|
||||||
|
@ -85,7 +87,12 @@ class SharedLinkItem extends ConsumerWidget {
|
||||||
).then((_) {
|
).then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
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),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -162,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget {
|
||||||
Widget buildBottomInfo() {
|
Widget buildBottomInfo() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (sharedLink.allowUpload) buildInfoChip("shared_link_info_chip_upload".tr()),
|
if (sharedLink.allowUpload)
|
||||||
if (sharedLink.allowDownload) buildInfoChip("shared_link_info_chip_download".tr()),
|
buildInfoChip("shared_link_info_chip_upload".tr()),
|
||||||
if (sharedLink.showMetadata) buildInfoChip("shared_link_info_chip_metadata".tr()),
|
if (sharedLink.allowDownload)
|
||||||
|
buildInfoChip("shared_link_info_chip_download".tr()),
|
||||||
|
if (sharedLink.showMetadata)
|
||||||
|
buildInfoChip("shared_link_info_chip_metadata".tr()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,7 +275,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||||
).then((_) {
|
).then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
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),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.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/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.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 {
|
class SharedLinkPage extends HookConsumerWidget {
|
||||||
const SharedLinkPage({Key? key}) : super(key: key);
|
const SharedLinkPage({Key? key}) : super(key: key);
|
||||||
|
@ -18,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget {
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
|
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
|
||||||
return () => ref.invalidate(sharedLinksStateProvider);
|
return () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
@ -113,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget {
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: sharedLinks.when(
|
child: sharedLinks.widgetWhen(
|
||||||
data: (links) =>
|
onError: (error, stackTrace) => buildNoShares(),
|
||||||
|
onData: (links) =>
|
||||||
links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
|
links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
|
||||||
error: (error, stackTrace) => buildNoShares(),
|
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.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/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/confirm_dialog.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/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
class TrashPage extends HookConsumerWidget {
|
class TrashPage extends HookConsumerWidget {
|
||||||
const TrashPage({super.key});
|
const TrashPage({super.key});
|
||||||
|
@ -24,7 +25,7 @@ class TrashPage extends HookConsumerWidget {
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
|
ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
final selection = useState(<Asset>{});
|
final selection = useState(<Asset>{});
|
||||||
final processing = useState(false);
|
final processing = useProcessingOverlay();
|
||||||
|
|
||||||
void selectionListener(
|
void selectionListener(
|
||||||
bool multiselect,
|
bool multiselect,
|
||||||
|
@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return trashedAssets.when(
|
return Scaffold(
|
||||||
loading: () => Scaffold(
|
appBar: trashedAssets.maybeWhen(
|
||||||
appBar: buildAppBar("?"),
|
orElse: () => buildAppBar("?"),
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
data: (data) => buildAppBar(data.totalAssets.toString()),
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => Scaffold(
|
body: trashedAssets.widgetWhen(
|
||||||
appBar: buildAppBar("!"),
|
onData: (data) => data.isEmpty
|
||||||
body: Center(child: Text(error.toString())),
|
|
||||||
),
|
|
||||||
data: (data) => Scaffold(
|
|
||||||
appBar: buildAppBar(data.totalAssets.toString()),
|
|
||||||
body: data.isEmpty
|
|
||||||
? Center(
|
? Center(
|
||||||
child: Text('trash_page_no_assets'.tr()),
|
child: Text('trash_page_no_assets'.tr()),
|
||||||
)
|
)
|
||||||
|
@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget {
|
||||||
showMultiSelectIndicator: false,
|
showMultiSelectIndicator: false,
|
||||||
showStack: true,
|
showStack: true,
|
||||||
topWidget: Padding(
|
topWidget: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(
|
||||||
top: 24,
|
horizontal: 12,
|
||||||
bottom: 24,
|
vertical: 24,
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"trash_page_info",
|
"trash_page_info",
|
||||||
|
@ -267,8 +261,6 @@ class TrashPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (selectionEnabledHook.value) buildBottomBar(),
|
if (selectionEnabledHook.value) buildBottomBar(),
|
||||||
if (processing.value)
|
|
||||||
const Center(child: ImmichLoadingIndicator()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
31
mobile/lib/routing/custom_transition_builders.dart
Normal file
31
mobile/lib/routing/custom_transition_builders.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomTransitionsBuilders {
|
||||||
|
const CustomTransitionsBuilders._();
|
||||||
|
|
||||||
|
static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder =
|
||||||
|
ZoomPageTransitionsBuilder();
|
||||||
|
|
||||||
|
static const RouteTransitionsBuilder zoomedPage = _zoomedPage;
|
||||||
|
|
||||||
|
static Widget _zoomedPage(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return zoomPageTransitionsBuilder.buildTransitions(
|
||||||
|
// Empty PageRoute<> object, only used to pass allowSnapshotting to ZoomPageTransitionsBuilder
|
||||||
|
PageRouteBuilder(
|
||||||
|
allowSnapshotting: true,
|
||||||
|
fullscreenDialog: false,
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
animation,
|
||||||
|
secondaryAnimation,
|
||||||
|
child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/map/ui/map_location_picker.dart';
|
||||||
import 'package:immich_mobile/modules/map/views/map_page.dart';
|
import 'package:immich_mobile/modules/map/views/map_page.dart';
|
||||||
import 'package:immich_mobile/modules/memories/models/memory.dart';
|
import 'package:immich_mobile/modules/memories/models/memory.dart';
|
||||||
import 'package:immich_mobile/modules/memories/views/memory_page.dart';
|
import 'package:immich_mobile/modules/memories/views/memory_page.dart';
|
||||||
|
@ -43,6 +44,7 @@ import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
|
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
@ -56,7 +58,8 @@ import 'package:immich_mobile/shared/views/app_log_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
|
@ -86,9 +89,10 @@ part 'router.gr.dart';
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
CustomRoute(
|
||||||
page: GalleryViewerPage,
|
page: GalleryViewerPage,
|
||||||
guards: [AuthGuard, DuplicateGuard],
|
guards: [AuthGuard, DuplicateGuard],
|
||||||
|
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||||
),
|
),
|
||||||
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
|
@ -170,6 +174,10 @@ part 'router.gr.dart';
|
||||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
durationInMilliseconds: 200,
|
durationInMilliseconds: 200,
|
||||||
),
|
),
|
||||||
|
CustomRoute<LatLng?>(
|
||||||
|
page: MapLocationPickerPage,
|
||||||
|
guards: [AuthGuard, DuplicateGuard],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
|
|
@ -63,7 +63,7 @@ class _$AppRouter extends RootStackRouter {
|
||||||
},
|
},
|
||||||
GalleryViewerRoute.name: (routeData) {
|
GalleryViewerRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<GalleryViewerRouteArgs>();
|
final args = routeData.argsAs<GalleryViewerRouteArgs>();
|
||||||
return MaterialPageX<dynamic>(
|
return CustomPage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: GalleryViewerPage(
|
child: GalleryViewerPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
|
@ -75,6 +75,9 @@ class _$AppRouter extends RootStackRouter {
|
||||||
isOwner: args.isOwner,
|
isOwner: args.isOwner,
|
||||||
sharedAlbumId: args.sharedAlbumId,
|
sharedAlbumId: args.sharedAlbumId,
|
||||||
),
|
),
|
||||||
|
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
VideoViewerRoute.name: (routeData) {
|
VideoViewerRoute.name: (routeData) {
|
||||||
|
@ -357,6 +360,19 @@ class _$AppRouter extends RootStackRouter {
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
MapLocationPickerRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<MapLocationPickerRouteArgs>(
|
||||||
|
orElse: () => const MapLocationPickerRouteArgs());
|
||||||
|
return CustomPage<LatLng?>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: MapLocationPickerPage(
|
||||||
|
key: args.key,
|
||||||
|
initialLatLng: args.initialLatLng,
|
||||||
|
),
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
|
@ -701,6 +717,14 @@ class _$AppRouter extends RootStackRouter {
|
||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
RouteConfig(
|
||||||
|
MapLocationPickerRoute.name,
|
||||||
|
path: '/map-location-picker-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1618,6 +1642,40 @@ class ActivitiesRouteArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [MapLocationPickerPage]
|
||||||
|
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||||
|
MapLocationPickerRoute({
|
||||||
|
Key? key,
|
||||||
|
LatLng? initialLatLng,
|
||||||
|
}) : super(
|
||||||
|
MapLocationPickerRoute.name,
|
||||||
|
path: '/map-location-picker-page',
|
||||||
|
args: MapLocationPickerRouteArgs(
|
||||||
|
key: key,
|
||||||
|
initialLatLng: initialLatLng,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'MapLocationPickerRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLocationPickerRouteArgs {
|
||||||
|
const MapLocationPickerRouteArgs({
|
||||||
|
this.key,
|
||||||
|
this.initialLatLng,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final LatLng? initialLatLng;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
@ -256,6 +256,8 @@ class Asset {
|
||||||
isFavorite != a.isFavorite ||
|
isFavorite != a.isFavorite ||
|
||||||
isArchived != a.isArchived ||
|
isArchived != a.isArchived ||
|
||||||
isTrashed != a.isTrashed ||
|
isTrashed != a.isTrashed ||
|
||||||
|
a.exifInfo?.latitude != exifInfo?.latitude ||
|
||||||
|
a.exifInfo?.longitude != exifInfo?.longitude ||
|
||||||
// no local stack count or different count from remote
|
// no local stack count or different count from remote
|
||||||
((stackCount == null && a.stackCount != null) ||
|
((stackCount == null && a.stackCount != null) ||
|
||||||
(stackCount != null &&
|
(stackCount != null &&
|
||||||
|
|
2
mobile/lib/shared/providers/api.provider.g.dart
generated
2
mobile/lib/shared/providers/api.provider.g.dart
generated
|
@ -6,7 +6,7 @@ part of 'api.provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d';
|
String _$apiServiceHash() => r'5b8beddb448316bdae5e3963ff77601653715729';
|
||||||
|
|
||||||
/// See also [apiService].
|
/// See also [apiService].
|
||||||
@ProviderFor(apiService)
|
@ProviderFor(apiService)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info/server_version.model.dart';
|
import 'package:immich_mobile/shared/models/server_info/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
|
@ -14,13 +17,33 @@ import 'package:socket_io_client/socket_io_client.dart';
|
||||||
|
|
||||||
enum PendingAction {
|
enum PendingAction {
|
||||||
assetDelete,
|
assetDelete,
|
||||||
|
assetUploaded,
|
||||||
|
assetHidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
class PendingChange {
|
class PendingChange {
|
||||||
|
final String id;
|
||||||
final PendingAction action;
|
final PendingAction action;
|
||||||
final dynamic value;
|
final dynamic value;
|
||||||
|
|
||||||
const PendingChange(this.action, this.value);
|
const PendingChange(
|
||||||
|
this.id,
|
||||||
|
this.action,
|
||||||
|
this.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'PendingChange(id: $id, action: $action, value: $value)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is PendingChange && other.id == id && other.action == action;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode ^ action.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketState {
|
class WebsocketState {
|
||||||
|
@ -131,6 +154,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
socket.on('on_asset_trash', _handleServerUpdates);
|
socket.on('on_asset_trash', _handleServerUpdates);
|
||||||
socket.on('on_asset_restore', _handleServerUpdates);
|
socket.on('on_asset_restore', _handleServerUpdates);
|
||||||
socket.on('on_asset_update', _handleServerUpdates);
|
socket.on('on_asset_update', _handleServerUpdates);
|
||||||
|
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||||
socket.on('on_new_release', _handleReleaseUpdates);
|
socket.on('on_new_release', _handleReleaseUpdates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||||
|
@ -163,35 +187,78 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPendingChange(PendingAction action, dynamic value) {
|
void addPendingChange(PendingAction action, dynamic value) {
|
||||||
|
final now = DateTime.now();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
|
pendingChanges: [
|
||||||
|
...state.pendingChanges,
|
||||||
|
PendingChange(now.millisecondsSinceEpoch.toString(), action, value),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
_debounce(handlePendingChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handlePendingChanges() {
|
Future<void> _handlePendingDeletes() async {
|
||||||
final deleteChanges = state.pendingChanges
|
final deleteChanges = state.pendingChanges
|
||||||
.where((c) => c.action == PendingAction.assetDelete)
|
.where((c) => c.action == PendingAction.assetDelete)
|
||||||
.toList();
|
.toList();
|
||||||
if (deleteChanges.isNotEmpty) {
|
if (deleteChanges.isNotEmpty) {
|
||||||
List<String> remoteIds =
|
List<String> remoteIds =
|
||||||
deleteChanges.map((a) => a.value.toString()).toList();
|
deleteChanges.map((a) => a.value.toString()).toList();
|
||||||
_ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
pendingChanges: state.pendingChanges
|
pendingChanges: state.pendingChanges
|
||||||
.where((c) => c.action != PendingAction.assetDelete)
|
.whereNot((c) => deleteChanges.contains(c))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnUploadSuccess(dynamic data) {
|
Future<void> _handlePendingUploaded() async {
|
||||||
final dto = AssetResponseDto.fromJson(data);
|
final uploadedChanges = state.pendingChanges
|
||||||
if (dto != null) {
|
.where((c) => c.action == PendingAction.assetUploaded)
|
||||||
final newAsset = Asset.remote(dto);
|
.toList();
|
||||||
_ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
if (uploadedChanges.isNotEmpty) {
|
||||||
|
List<AssetResponseDto?> remoteAssets = uploadedChanges
|
||||||
|
.map((a) => AssetResponseDto.fromJson(a.value))
|
||||||
|
.toList();
|
||||||
|
for (final dto in remoteAssets) {
|
||||||
|
if (dto != null) {
|
||||||
|
final newAsset = Asset.remote(dto);
|
||||||
|
await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = state.copyWith(
|
||||||
|
pendingChanges: state.pendingChanges
|
||||||
|
.whereNot((c) => uploadedChanges.contains(c))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handlingPendingHidden() async {
|
||||||
|
final hiddenChanges = state.pendingChanges
|
||||||
|
.where((c) => c.action == PendingAction.assetHidden)
|
||||||
|
.toList();
|
||||||
|
if (hiddenChanges.isNotEmpty) {
|
||||||
|
List<String> remoteIds =
|
||||||
|
hiddenChanges.map((a) => a.value.toString()).toList();
|
||||||
|
final db = _ref.watch(dbProvider);
|
||||||
|
await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds));
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
pendingChanges: state.pendingChanges
|
||||||
|
.whereNot((c) => hiddenChanges.contains(c))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePendingChanges() async {
|
||||||
|
await _handlePendingUploaded();
|
||||||
|
await _handlePendingDeletes();
|
||||||
|
await _handlingPendingHidden();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleOnConfigUpdate(dynamic _) {
|
void _handleOnConfigUpdate(dynamic _) {
|
||||||
_ref.read(serverInfoProvider.notifier).getServerFeatures();
|
_ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||||
_ref.read(serverInfoProvider.notifier).getServerConfig();
|
_ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
|
@ -202,10 +269,14 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
_ref.read(assetProvider.notifier).getAllAsset();
|
_ref.read(assetProvider.notifier).getAllAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnAssetDelete(dynamic data) {
|
void _handleOnUploadSuccess(dynamic data) =>
|
||||||
addPendingChange(PendingAction.assetDelete, data);
|
addPendingChange(PendingAction.assetUploaded, data);
|
||||||
_debounce(handlePendingChanges);
|
|
||||||
}
|
void _handleOnAssetDelete(dynamic data) =>
|
||||||
|
addPendingChange(PendingAction.assetDelete, data);
|
||||||
|
|
||||||
|
void _handleOnAssetHidden(dynamic data) =>
|
||||||
|
addPendingChange(PendingAction.assetHidden, data);
|
||||||
|
|
||||||
_handleReleaseUpdates(dynamic data) {
|
_handleReleaseUpdates(dynamic data) {
|
||||||
// Json guard
|
// Json guard
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
@ -181,4 +182,27 @@ class AssetService {
|
||||||
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
|
Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
|
||||||
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
|
return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Asset?>> changeDateTime(
|
||||||
|
List<Asset> assets,
|
||||||
|
String updatedDt,
|
||||||
|
) {
|
||||||
|
return updateAssets(
|
||||||
|
assets,
|
||||||
|
UpdateAssetDto(dateTimeOriginal: updatedDt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Asset?>> changeLocation(
|
||||||
|
List<Asset> assets,
|
||||||
|
LatLng location,
|
||||||
|
) {
|
||||||
|
return updateAssets(
|
||||||
|
assets,
|
||||||
|
UpdateAssetDto(
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -401,6 +401,10 @@ class SyncService {
|
||||||
|
|
||||||
final Album a = await Album.remote(dto);
|
final Album a = await Album.remote(dto);
|
||||||
await _db.writeTxn(() => _db.albums.store(a));
|
await _db.writeTxn(() => _db.albums.store(a));
|
||||||
|
} else {
|
||||||
|
_log.warning(
|
||||||
|
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
||||||
|
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
260
mobile/lib/shared/ui/date_time_picker.dart
Normal file
260
mobile/lib/shared/ui/date_time_picker.dart
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
import 'package:timezone/timezone.dart';
|
||||||
|
|
||||||
|
Future<String?> showDateTimePicker({
|
||||||
|
required BuildContext context,
|
||||||
|
DateTime? initialDateTime,
|
||||||
|
String? initialTZ,
|
||||||
|
Duration? initialTZOffset,
|
||||||
|
}) {
|
||||||
|
return showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _DateTimePicker(
|
||||||
|
initialDateTime: initialDateTime,
|
||||||
|
initialTZ: initialTZ,
|
||||||
|
initialTZOffset: initialTZOffset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
||||||
|
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateTimePicker extends HookWidget {
|
||||||
|
final DateTime? initialDateTime;
|
||||||
|
final String? initialTZ;
|
||||||
|
final Duration? initialTZOffset;
|
||||||
|
|
||||||
|
const _DateTimePicker({
|
||||||
|
this.initialDateTime,
|
||||||
|
this.initialTZ,
|
||||||
|
this.initialTZOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
_TimeZoneOffset _getInitiationLocation() {
|
||||||
|
if (initialTZ != null) {
|
||||||
|
try {
|
||||||
|
return _TimeZoneOffset.fromLocation(
|
||||||
|
tz.timeZoneDatabase.get(initialTZ!),
|
||||||
|
);
|
||||||
|
} on LocationNotFoundException {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset;
|
||||||
|
|
||||||
|
if (tzOffset != null) {
|
||||||
|
final offsetInMilli = tzOffset.inMilliseconds;
|
||||||
|
// get all locations with matching offset
|
||||||
|
final locations = tz.timeZoneDatabase.locations.values.where(
|
||||||
|
(location) => location.currentTimeZone.offset == offsetInMilli,
|
||||||
|
);
|
||||||
|
// Prefer locations with abbreviation first
|
||||||
|
final location = locations.firstWhereOrNull(
|
||||||
|
(e) => !e.currentTimeZone.abbreviation.contains("0"),
|
||||||
|
) ??
|
||||||
|
locations.firstOrNull;
|
||||||
|
if (location != null) {
|
||||||
|
return _TimeZoneOffset.fromLocation(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _TimeZoneOffset.fromLocation(tz.getLocation("UTC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a list of location<name> along with it's offset in duration
|
||||||
|
List<_TimeZoneOffset> getAllTimeZones() {
|
||||||
|
return tz.timeZoneDatabase.locations.values
|
||||||
|
.where((l) => !l.currentTimeZone.abbreviation.contains("0"))
|
||||||
|
.map(_TimeZoneOffset.fromLocation)
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final date = useState<DateTime>(initialDateTime ?? DateTime.now());
|
||||||
|
final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation());
|
||||||
|
final timeZones = useMemoized(() => getAllTimeZones(), const []);
|
||||||
|
|
||||||
|
void pickDate() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
// Handles cases where the date from the asset is far off in the future
|
||||||
|
final initialDate = date.value.isAfter(now) ? now : date.value;
|
||||||
|
final newDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: DateTime(1800),
|
||||||
|
lastDate: now,
|
||||||
|
);
|
||||||
|
if (newDate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newTime = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.fromDateTime(date.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newTime == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
date.value = newDate.copyWith(hour: newTime.hour, minute: newTime.minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
void popWithDateTime() {
|
||||||
|
final formattedDateTime =
|
||||||
|
DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value);
|
||||||
|
final dtWithOffset = formattedDateTime +
|
||||||
|
Duration(milliseconds: tzOffset.value.offsetInMilliseconds)
|
||||||
|
.formatAsOffset();
|
||||||
|
context.pop(dtWithOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
contentPadding: const EdgeInsets.all(30),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"edit_date_time_dialog_date_time",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: pickDate,
|
||||||
|
icon: Text(
|
||||||
|
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
|
||||||
|
style: context.textTheme.bodyLarge
|
||||||
|
?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
label: const Icon(
|
||||||
|
Icons.edit_outlined,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
"edit_date_time_dialog_timezone",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
DropdownMenu(
|
||||||
|
menuHeight: 300,
|
||||||
|
width: 280,
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
trailingIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
menuStyle: const MenuStyle(
|
||||||
|
fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
|
||||||
|
alignment: Alignment(-1.25, 0.5),
|
||||||
|
),
|
||||||
|
onSelected: (value) => tzOffset.value = value!,
|
||||||
|
initialSelection: tzOffset.value,
|
||||||
|
dropdownMenuEntries: timeZones
|
||||||
|
.map(
|
||||||
|
(t) => DropdownMenuEntry<_TimeZoneOffset>(
|
||||||
|
value: t,
|
||||||
|
label: t.display,
|
||||||
|
style: ButtonStyle(
|
||||||
|
textStyle: MaterialStatePropertyAll(
|
||||||
|
context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text(
|
||||||
|
"action_common_cancel",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: popWithDateTime,
|
||||||
|
child: Text(
|
||||||
|
"action_common_update",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
|
||||||
|
final String display;
|
||||||
|
final Location location;
|
||||||
|
|
||||||
|
const _TimeZoneOffset({
|
||||||
|
required this.display,
|
||||||
|
required this.location,
|
||||||
|
});
|
||||||
|
|
||||||
|
_TimeZoneOffset copyWith({
|
||||||
|
String? display,
|
||||||
|
Location? location,
|
||||||
|
}) {
|
||||||
|
return _TimeZoneOffset(
|
||||||
|
display: display ?? this.display,
|
||||||
|
location: location ?? this.location,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get offsetInMilliseconds => location.currentTimeZone.offset;
|
||||||
|
|
||||||
|
_TimeZoneOffset.fromLocation(tz.Location l)
|
||||||
|
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
|
||||||
|
location = l;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(_TimeZoneOffset other) {
|
||||||
|
return offsetInMilliseconds.compareTo(other.offsetInMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'_TimeZoneOffset(display: $display, location: $location)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is _TimeZoneOffset &&
|
||||||
|
other.display == display &&
|
||||||
|
other.offsetInMilliseconds == offsetInMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode;
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
child: const CircularProgressIndicator(
|
child: const CircularProgressIndicator(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
strokeWidth: 2,
|
strokeWidth: 3,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
256
mobile/lib/shared/ui/location_picker.dart
Normal file
256
mobile/lib/shared/ui/location_picker.dart
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_map/plugin_api.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
Future<LatLng?> showLocationPicker({
|
||||||
|
required BuildContext context,
|
||||||
|
LatLng? initialLatLng,
|
||||||
|
}) {
|
||||||
|
return showDialog<LatLng?>(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: false,
|
||||||
|
builder: (ctx) => _LocationPicker(
|
||||||
|
initialLatLng: initialLatLng,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _LocationPickerMode { map, manual }
|
||||||
|
|
||||||
|
bool _validateLat(String value) {
|
||||||
|
final l = double.tryParse(value);
|
||||||
|
return l != null && l > -90 && l < 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validateLong(String value) {
|
||||||
|
final l = double.tryParse(value);
|
||||||
|
return l != null && l > -180 && l < 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocationPicker extends HookWidget {
|
||||||
|
final LatLng? initialLatLng;
|
||||||
|
|
||||||
|
const _LocationPicker({
|
||||||
|
this.initialLatLng,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final latitude = useState(initialLatLng?.latitude ?? 0.0);
|
||||||
|
final longitude = useState(initialLatLng?.longitude ?? 0.0);
|
||||||
|
final latlng = LatLng(latitude.value, longitude.value);
|
||||||
|
final pickerMode = useState(_LocationPickerMode.map);
|
||||||
|
final latitudeController = useTextEditingController();
|
||||||
|
final isValidLatitude = useState(true);
|
||||||
|
final latitiudeFocusNode = useFocusNode();
|
||||||
|
final longitudeController = useTextEditingController();
|
||||||
|
final longitudeFocusNode = useFocusNode();
|
||||||
|
final isValidLongitude = useState(true);
|
||||||
|
|
||||||
|
void validateInputs() {
|
||||||
|
isValidLatitude.value = _validateLat(latitudeController.text);
|
||||||
|
if (isValidLatitude.value) {
|
||||||
|
latitude.value = latitudeController.text.toDouble();
|
||||||
|
}
|
||||||
|
isValidLongitude.value = _validateLong(longitudeController.text);
|
||||||
|
if (isValidLongitude.value) {
|
||||||
|
longitude.value = longitudeController.text.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void validateAndPop() {
|
||||||
|
if (pickerMode.value == _LocationPickerMode.manual) {
|
||||||
|
validateInputs();
|
||||||
|
}
|
||||||
|
if (isValidLatitude.value && isValidLongitude.value) {
|
||||||
|
return context.pop(latlng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> buildMapPickerMode() {
|
||||||
|
return [
|
||||||
|
TextButton.icon(
|
||||||
|
icon: Text(
|
||||||
|
"${latitude.value.toStringAsFixed(4)}, ${longitude.value.toStringAsFixed(4)}",
|
||||||
|
),
|
||||||
|
label: const Icon(Icons.edit_outlined, size: 16),
|
||||||
|
onPressed: () {
|
||||||
|
latitudeController.text = latitude.value.toStringAsFixed(4);
|
||||||
|
longitudeController.text = longitude.value.toStringAsFixed(4);
|
||||||
|
pickerMode.value = _LocationPickerMode.manual;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
MapThumbnail(
|
||||||
|
coords: latlng,
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
zoom: 6,
|
||||||
|
showAttribution: false,
|
||||||
|
onTap: (p0, p1) async {
|
||||||
|
final newLatLng = await context.autoPush<LatLng?>(
|
||||||
|
MapLocationPickerRoute(initialLatLng: latlng),
|
||||||
|
);
|
||||||
|
if (newLatLng != null) {
|
||||||
|
latitude.value = newLatLng.latitude;
|
||||||
|
longitude.value = newLatLng.longitude;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markers: [
|
||||||
|
Marker(
|
||||||
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
|
point: LatLng(
|
||||||
|
latitude.value,
|
||||||
|
longitude.value,
|
||||||
|
),
|
||||||
|
builder: (ctx) => const Image(
|
||||||
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> buildManualPickerMode() {
|
||||||
|
return [
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Text("location_picker_choose_on_map").tr(),
|
||||||
|
label: const Icon(Icons.map_outlined, size: 16),
|
||||||
|
onPressed: () {
|
||||||
|
validateInputs();
|
||||||
|
if (isValidLatitude.value && isValidLongitude.value) {
|
||||||
|
pickerMode.value = _LocationPickerMode.map;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: latitudeController,
|
||||||
|
focusNode: latitiudeFocusNode,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'location_picker_latitude'.tr(),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'location_picker_latitude_hint'.tr(),
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
errorText: isValidLatitude.value
|
||||||
|
? null
|
||||||
|
: "location_picker_latitude_error".tr(),
|
||||||
|
),
|
||||||
|
onEditingComplete: () {
|
||||||
|
isValidLatitude.value = _validateLat(latitudeController.text);
|
||||||
|
if (isValidLatitude.value) {
|
||||||
|
latitude.value = latitudeController.text.toDouble();
|
||||||
|
longitudeFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: [LengthLimitingTextInputFormatter(8)],
|
||||||
|
onTapOutside: (_) => latitiudeFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: longitudeController,
|
||||||
|
focusNode: longitudeFocusNode,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'location_picker_longitude'.tr(),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'location_picker_longitude_hint'.tr(),
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
errorText: isValidLongitude.value
|
||||||
|
? null
|
||||||
|
: "location_picker_longitude_error".tr(),
|
||||||
|
),
|
||||||
|
onEditingComplete: () {
|
||||||
|
isValidLongitude.value = _validateLong(longitudeController.text);
|
||||||
|
if (isValidLongitude.value) {
|
||||||
|
longitude.value = longitudeController.text.toDouble();
|
||||||
|
longitudeFocusNode.unfocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: [LengthLimitingTextInputFormatter(8)],
|
||||||
|
onTapOutside: (_) => longitudeFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
contentPadding: const EdgeInsets.all(30),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"edit_location_dialog_title",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
if (pickerMode.value == _LocationPickerMode.manual)
|
||||||
|
...buildManualPickerMode(),
|
||||||
|
if (pickerMode.value == _LocationPickerMode.map)
|
||||||
|
...buildMapPickerMode(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text(
|
||||||
|
"action_common_cancel",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: validateAndPop,
|
||||||
|
child: Text(
|
||||||
|
"action_common_update",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
// Error widget to be used in Scaffold when an AsyncError is received
|
// Error widget to be used in Scaffold when an AsyncError is received
|
||||||
class ScaffoldErrorBody extends StatelessWidget {
|
class ScaffoldErrorBody extends StatelessWidget {
|
||||||
final IconData icon;
|
final bool withIcon;
|
||||||
|
|
||||||
const ScaffoldErrorBody({this.icon = Icons.error_outline, super.key});
|
const ScaffoldErrorBody({super.key, this.withIcon = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -14,19 +14,22 @@ class ScaffoldErrorBody extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"scaffold_body_error_occured",
|
"scaffold_body_error_occurred",
|
||||||
style:
|
style: context.textTheme.displayMedium,
|
||||||
TextStyle(fontSize: 14, fontWeight: FontWeight.bold, height: 3),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).tr(),
|
).tr(),
|
||||||
Center(
|
if (withIcon)
|
||||||
child: Icon(
|
Center(
|
||||||
icon,
|
child: Padding(
|
||||||
size: 100,
|
padding: const EdgeInsets.only(top: 15),
|
||||||
color: context.themeData.iconTheme.color?.withOpacity(0.5),
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 100,
|
||||||
|
color: context.themeData.iconTheme.color?.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,14 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||||
Clipboard.setData(ClipboardData(text: stackTrace))
|
Clipboard.setData(ClipboardData(text: stackTrace))
|
||||||
.then((_) {
|
.then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: message)).then((_) {
|
Clipboard.setData(ClipboardData(text: message)).then((_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,41 +1,64 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
class ImmichLoadingOverlay extends StatelessWidget {
|
final _loadingEntry = OverlayEntry(
|
||||||
const ImmichLoadingOverlay({
|
builder: (context) => SizedBox.square(
|
||||||
Key? key,
|
dimension: double.infinity,
|
||||||
}) : super(key: key);
|
child: DecoratedBox(
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
||||||
|
child: const Center(child: ImmichLoadingIndicator()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ValueNotifier<bool> useProcessingOverlay() {
|
||||||
|
return use(const _LoadingOverlay());
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
|
||||||
|
const _LoadingOverlay();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
_LoadingOverlayState createState() => _LoadingOverlayState();
|
||||||
return ValueListenableBuilder<bool>(
|
|
||||||
valueListenable:
|
|
||||||
ImmichLoadingOverlayController.appLoader.loaderShowingNotifier,
|
|
||||||
builder: (context, shouldShow, child) {
|
|
||||||
return shouldShow
|
|
||||||
? const Scaffold(
|
|
||||||
backgroundColor: Colors.black54,
|
|
||||||
body: Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichLoadingOverlayController {
|
class _LoadingOverlayState
|
||||||
static final ImmichLoadingOverlayController appLoader =
|
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
||||||
ImmichLoadingOverlayController();
|
late final _isProcessing = ValueNotifier(false)..addListener(_listener);
|
||||||
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
|
OverlayEntry? overlayEntry;
|
||||||
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
|
|
||||||
|
|
||||||
void show() {
|
void _listener() {
|
||||||
loaderShowingNotifier.value = true;
|
setState(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_isProcessing.value) {
|
||||||
|
overlayEntry?.remove();
|
||||||
|
overlayEntry = _loadingEntry;
|
||||||
|
Overlay.of(context).insert(_loadingEntry);
|
||||||
|
} else {
|
||||||
|
overlayEntry?.remove();
|
||||||
|
overlayEntry = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void hide() {
|
@override
|
||||||
loaderShowingNotifier.value = false;
|
ValueNotifier<bool> build(BuildContext context) {
|
||||||
|
return _isProcessing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isProcessing.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? get debugValue => _isProcessing.value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugLabel => 'useProcessingOverlay<>';
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue