Compare commits

...

45 Commits

Author SHA1 Message Date
Alex Tran
1ba998aa68 Added changlog for Fdroid release 2022-07-27 13:17:36 -05:00
Alex Tran
2de34f70ce Update readme 2022-07-27 13:09:52 -05:00
Alex Tran
8b9fd67d6f Remove AxiosError import due to production build error 2022-07-27 13:01:49 -05:00
Alex
97238a1621 Up version for release 2022-07-27 11:39:19 -05:00
Alex
ef4136d327 [WEB] Select album thumbnail (#383)
* Added context menu for album opionts

* choose asset for album thumbnail

* Refactor UpdateAlbumDto to accept albumThumbnailAssetId

* implemented changing album cover on web

* Fixed api change on mobile app
2022-07-27 11:16:02 -05:00
Alex
6dbca8d478 Added Japanese/Polish/Finish and fix Italian/Spanish translation 2022-07-27 11:14:21 -05:00
Alex
a305db9e6f [Localizely] Translations update (#384) 2022-07-27 11:07:37 -05:00
Alex Tran
59c1ea3097 Added 2-stage loading for album's thumbnail 2022-07-26 22:06:06 -05:00
Alex
03457f5d32 [WEB] Upload asset directly to album (#379)
* Added stores to get album assetId

* Upload assets and add to album

* Added comments

* resolve conflict when add assets from upload directly

* Filtered out duplicate asset before adding to the album
2022-07-26 20:53:25 -05:00
Alex
2336a6159c [WEB] Load thumbnail with native source property for faster load time (#378) 2022-07-26 15:13:08 -05:00
Alex Tran
e4c4b53fcd Added imageName as searchable text on database 2022-07-26 13:43:12 -05:00
Alex
83cbf51704 Use cookies for client requests (#377)
* Use cookie for frontend request

* Remove api helper to use SDK

* Added error handling to status box

* Remove additional places that check for session.user

* Refactor sending password

* prettier clean up

* remove deadcode

* Move all authentication requests to the client

* refactor upload panel to only fetch assets after the upload panel disappear

* Added keydown to remove focus on title change on album viewer
2022-07-26 12:28:07 -05:00
Alex Tran
2ebb755f00 Fixed corner radius for message board 2022-07-24 23:45:05 -05:00
Alex Tran
ec1c3a86f5 Added messages when there is no album or shared album 2022-07-24 23:30:30 -05:00
Alex
969f770df0 Delete album on web (#373)
* Show context menu

* Show context menu at the correct location

* Implement delete album button

* Delete album within album viewer
2022-07-24 22:47:12 -05:00
Alex Tran
9c3f848fa8 Use Webp for album thumbnail 2022-07-24 08:51:00 -05:00
Alex Tran
1ea6425cd1 Handle unhandled promises that lead to unable to login 2022-07-24 08:41:06 -05:00
Alex
052db5d748 Remove/Add asset in ablum on web (#371)
* Added interaction to select multiple thumbnail

* Fixed stutter transition

* Return AlbumResponseDto after removing an asset from album

* Render correctly when an array of thumbnail is updated

* Fixed wording

* Added native dialog for removing users from album

* Fixed rendering incorrect profile image on share user select dialog
2022-07-23 23:23:14 -05:00
bo0tzz
a35460cb84 Bump tfjs version to 3.19.0 for arm64 support (#368)
* Add linux/arm64 to machine-learning container build

* Bump tfjs version to 3.19.0

* Fix tfjs dependency error
2022-07-23 14:15:55 -05:00
Alex
ae93bbe2a7 Docker login only with branch from the repository (#370) 2022-07-23 13:48:53 -05:00
Alex
3b97c7729b Implement mechanism to remove and add shared user in album on web (#369)
* AFixed overlay issue of modal

* Added modal with existing user

* Added custom scrollbar to all pages

* Fixed Document is not define when access document DOM node in browswer

* Added context menu

* Added api to remove user from album

* Handle user leave album

* Added share button to non-shared album

* Added padding to album viewer:

* Fixed margin top of asset selection page

* Fixed issue cannot push to dockerhub
2022-07-23 13:08:49 -05:00
bo0tzz
6021124688 Move docker login step to after build (#367) 2022-07-23 11:05:13 -05:00
Alex
1d34976dd0 Implement album creation on web (#365)
* Added album creation button functionality

* Added input for album title

* Added select photos button

* Added page to select assets

* Show photo selection timeline

* Implemented update album name mechanism:

* Added selection mechanism

* Added selection mechanism with existing assets in album

* Refactored and added comments

* Refactored and added comments - 2

* Refactor album app bar

* Added modal for select user

* Implemented choose users

* Added additional share user button

* Added rule to show add users button
2022-07-22 09:44:22 -05:00
dependabot[bot]
02bde51caf Bump docker/build-push-action from 3.0.0 to 3.1.0 (#363)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 20:49:11 -05:00
Matthias Rupp
bef1e2e3db Api logout route (#361)
* Add logout route that deletes http only cookies

* Rebuild API
2022-07-19 13:49:58 -05:00
Alex
be3e3e5d7e Added Cookie Authentication (#360)
* Added Cookie Authentication

* Fixed issue with bearer is in lower case

* Fixed bearer to Bearer to conform with standard
2022-07-18 14:14:25 -05:00
Alex
c028c7db4e dev/add detail viewer to album (#358)
* Rename asset viewer folder

* Refactor AssetViewer to be able to user with different component

* Refactor AssetViewer to be able to user with different component

* Added viewer for album and sharing
2022-07-18 00:22:39 -05:00
Alex Tran
c129023821 Remove console.log 2022-07-17 15:10:04 -05:00
Alex Tran
cbdb8fa51f Update get user info controller to avoid conflict with /count 2022-07-17 15:09:26 -05:00
Alex
c6ecfb679a Added sharing page to web (#355)
* Added shared album

* Added list tile

* Show info of shared album owner
2022-07-16 23:52:00 -05:00
Alex
5d03e9bda8 Fix test instance cannot clear database after each test" (#354)
* Update test

* Fixed test cannot initialize database

* Added a separate network to test containers group to run test while in development mode
2022-07-16 23:43:31 -05:00
Alex Tran
d8b26c6da8 Update bug report template 2022-07-16 10:54:00 -05:00
Alex
2e61cf3183 Update README.md
Fixed incorrect info about microservices container
2022-07-16 07:15:22 -05:00
Alex Tran
45e2335b86 Allow manually run test workflow 2022-07-16 00:48:35 -05:00
Alex Tran
2bbc44c5ab Fixed test 2022-07-16 00:45:58 -05:00
Alex Tran
012428416d Remove console.log 2022-07-15 23:27:23 -05:00
Alex
7134f93eb8 Add ablum feature to web (#352)
* Added album page

* Refactor sidebar

* Added album assets count info

* Added album viewer page

* Refactor album sorting

* Fixed incorrectly showing selected asset in album selection

* Improve fetching speed with prefetch

* Refactor to use ImmichThubmnail component for all

* Update to the latest version of Svelte

* Implement fixed app bar in album viewer

* Added shared user avatar

* Correctly get all owned albums, including shared
2022-07-15 23:18:17 -05:00
Jaime Baez
1887b5a860 Add email validation in the API when creating new users (#350)
* Refactor user.service - add user-repository

* Add email validation for creating users
2022-07-15 14:30:56 -05:00
Jaime Baez
ef17668871 Update Spanish translations (#348)
- add missing translations
- remove extra white spaces
- spelling and other corrections
2022-07-14 13:59:07 -05:00
Alex
e9909b179a Up version for release 2022-07-14 11:39:06 -05:00
Alex
09f8bdef6d Up version for release 2022-07-14 11:32:07 -05:00
Alex
2a9b09f359 Added DA,ES,FR,IT (#347)
* Added DA,ES,FR,IT

* Update French translation
2022-07-14 10:20:23 -05:00
Alex
1f6a3ccac7 Added local code in localizely config file 2022-07-14 08:39:43 -05:00
Eidenz
1f40fc1de9 Add missing translation tag for search_no_objects (#344)
* feat(mobile) added french translations

* fix(mobile) added missing translation tag for search_no_objects (EN,DE,FR)

* fix(mobile) renamed search_no_objects to search_page_no_objects
2022-07-13 20:46:28 -05:00
Eidenz
20b94ef0bb feat(mobile) added french translations (#343) 2022-07-13 11:35:02 -05:00
191 changed files with 7126 additions and 2698 deletions

View File

@@ -16,8 +16,11 @@ Note: Please search to see if an issue already exists for the bug you encountere
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**Task List** **Task List**
*Please complete the task list below. We need this information to help us reproduce the bug or point out problems in your setup. You are not providing enough info may delay our effort to help you.*
- [ ] I have read thoroughly the README setup and installation instructions. - [ ] I have read thoroughly the README setup and installation instructions.
- [ ] If my setup is different, I have included my docker-compose file. - [ ] I have included my `docker-compose` file.
- [ ] I have included my redacted `.env` file. - [ ] I have included my redacted `.env` file.
- [ ] I have included information on my machine, and environment. - [ ] I have included information on my machine, and environment.
@@ -34,13 +37,10 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **System**
- OS: [e.g. iOS] - Phone OS [iOS, Android]: `<version>`
- Server Version: `<version>`
**Smartphone (please complete the following information):** - Mobile App Version: `<version>`
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -27,7 +27,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
@@ -55,11 +55,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
altran1502/immich-machine-learning:latest altran1502/immich-machine-learning:latest
@@ -82,7 +82,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web - name: Build and Push Web
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
@@ -110,7 +110,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy - name: Build and Push Proxy
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile

View File

@@ -24,17 +24,18 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }} push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
tags: | tags: |
altran1502/immich-server:staging altran1502/immich-server:staging
@@ -52,17 +53,18 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }} push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
tags: | tags: |
altran1502/immich-machine-learning:staging altran1502/immich-machine-learning:staging
@@ -79,18 +81,19 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web - name: Build and Push Web
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod target: prod
push: ${{ github.event_name == 'pull_request' }} push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
tags: | tags: |
altran1502/immich-web:staging altran1502/immich-web:staging
@@ -107,16 +110,17 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy - name: Build and Push Proxy
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }} push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
tags: | tags: |
altran1502/immich-proxy:staging altran1502/immich-proxy:staging

View File

@@ -35,7 +35,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release - name: Build and push immich-server release
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
@@ -68,11 +68,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }} altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
@@ -107,7 +107,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release - name: Build and push immich-web release
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
@@ -147,7 +147,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release - name: Build and push immich-proxy release
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.1.0
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile

View File

@@ -1,5 +1,6 @@
name: Test name: Test
on: on:
workflow_dispatch:
pull_request: pull_request:
push: { branches: master } push: { branches: master }
@@ -14,4 +15,4 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Run Immich Server 2E2 Test - name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test

View File

@@ -10,11 +10,17 @@ dev-scale:
stage: stage:
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker-compose -f ./docker/docker-compose.staging.yml pull
test-e2e: test-e2e:
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich_server_test --remove-orphans docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
prod: prod:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
prod-scale: prod-scale:
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
api:
cd ./server && npm run api:generate

View File

@@ -65,22 +65,19 @@ This project is under heavy development, there will be continuous functions, fea
| Selective album(s) for backup | Yes | N/A | Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes | Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes | Multi-user support | Yes | Yes
| Shared Albums | Yes | No | Album | No | Yes
| Shared Albums | Yes | Yes
| Quick navigation with draggable scrollbar | Yes | Yes | Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes | Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| Metadata view (EXIF, map) | Yes | Yes | Metadata view (EXIF, map) | Yes | Yes
| Search by metadata, objects and image tags | Yes | No | Search by metadata, objects and image tags | Yes | No
| Administrative functions (user management) | No | Yes | Administrative functions (user management) | N/A | Yes
# System Requirement # System Requirement
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc). **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
**RAM**: At least 2GB, preffered 4GB. **RAM**: At least 2GB, preffered 4GB.
**Core**: At least 2 cores, preffered 4 cores. **Core**: At least 2 cores, preffered 4 cores.
@@ -248,7 +245,7 @@ Cheers! 🎉
## TensorFlow Build Issue ## TensorFlow Build Issue
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup* *This is a known issue for incorrect Promox setup*
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`: TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
@@ -261,7 +258,3 @@ If you are running virtualization in Promox, the VM doesn't have the flag enable
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab. You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host` `Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
Otherwise you can:
- edit `docker-compose.yml` file and comment the whole `immich-machine-learning` service **which will disable machine learning features like object detection and image classification**
- switch to a different VM/desktop with different architecture.

View File

@@ -1,5 +1,5 @@
# Database # Database
DB_HOSTNAME=immich_postgres_test DB_HOSTNAME=immich-database-test
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test DB_DATABASE_NAME=e2e_test

View File

@@ -70,6 +70,8 @@ services:
- ../web:/usr/src/app - ../web:/usr/src/app
- /usr/src/app/node_modules - /usr/src/app/node_modules
restart: always restart: always
depends_on:
- immich-server
redis: redis:
container_name: immich_redis container_name: immich_redis

View File

@@ -1,8 +1,8 @@
version: "3.8" version: "3.8"
services: services:
immich_server_test: immich-server-test:
image: immich-server-dev:latest image: immich-server-test
build: build:
context: ../server context: ../server
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -17,15 +17,17 @@ services:
environment: environment:
- NODE_ENV=development - NODE_ENV=development
depends_on: depends_on:
- redis - immich-redis-test
- database - immich-database-test
networks:
redis: - immich-test-network
container_name: immich_redis_test immich-redis-test:
container_name: immich-redis-test
image: redis:6.2 image: redis:6.2
networks:
database: - immich-test-network
container_name: immich_postgres_test immich-database-test:
container_name: immich-database-test
image: postgres:14 image: postgres:14
env_file: env_file:
- .env.test - .env.test
@@ -36,5 +38,8 @@ services:
PG_DATA: /var/lib/postgresql/data PG_DATA: /var/lib/postgresql/data
volumes: volumes:
- /var/lib/postgresql/data - /var/lib/postgresql/data
ports: networks:
- 5432:5432 - immich-test-network
networks:
immich-test-network:

View File

@@ -4,12 +4,16 @@ file_type: json
upload: upload:
files: files:
- file: mobile/assets/i18n/en-US.json - file: mobile/assets/i18n/en-US.json
locale_code: en locale_code: en-US
- file: mobile/assets/i18n/de-DE.json - file: mobile/assets/i18n/de-DE.json
locale_code: de locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
download: download:
files: files:
- file: mobile/assets/i18n/en-US.json - file: mobile/assets/i18n/en-US.json
locale_code: en locale_code: en-US
- file: mobile/assets/i18n/de-DE.json - file: mobile/assets/i18n/de-DE.json
locale_code: de locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR

View File

@@ -10,6 +10,7 @@ RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
RUN npm ci RUN npm ci
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
COPY . . COPY . .

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,11 @@
"@nestjs/typeorm": "^8.0.3", "@nestjs/typeorm": "^8.0.3",
"@tensorflow-models/coco-ssd": "^2.2.2", "@tensorflow-models/coco-ssd": "^2.2.2",
"@tensorflow-models/mobilenet": "^2.1.0", "@tensorflow-models/mobilenet": "^2.1.0",
"@tensorflow/tfjs": "^3.15.0", "@tensorflow/tfjs": "^3.19.0",
"@tensorflow/tfjs-converter": "^3.15.0", "@tensorflow/tfjs-converter": "^3.19.0",
"@tensorflow/tfjs-core": "^3.15.0", "@tensorflow/tfjs-core": "^3.19.0",
"@tensorflow/tfjs-node": "^3.15.0", "@tensorflow/tfjs-node": "^3.19.0",
"@tensorflow/tfjs-node-gpu": "^3.15.0", "@tensorflow/tfjs-node-gpu": "^3.19.0",
"@trpc/server": "^9.20.3", "@trpc/server": "^9.20.3",
"pg": "^8.7.3", "pg": "^8.7.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",

View File

@@ -1,2 +1,2 @@
json_key_file("/Users/alex/Documents/immich-fastlane-googleplaystore-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one json_key_file("/Users/alex/Documents/immich-play-store-key.json")
package_name("app.alextran.immich") # e.g. com.krausefx.app package_name("app.alextran.immich")

View File

@@ -16,10 +16,25 @@
default_platform(:android) default_platform(:android)
platform :android do platform :android do
desc "Build Android"
lane :build do
gradle(
task: 'bundle',
build_type: 'Release',
)
end
desc "Update AAB to PlayStore" desc "Build and Release Android"
lane :beta do lane :release do
upload_to_play_store(track: 'beta', aab: '../build/app/outputs/bundle/release/app-release.aab') gradle(
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 29,
"android.injected.version.name" => "1.19.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')
end end
end end

View File

@@ -15,10 +15,10 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## Android ## Android
### android beta ### android release
```sh ```sh
[bundle exec] fastlane android beta [bundle exec] fastlane android release
``` ```
Update AAB to PlayStore Update AAB to PlayStore

View File

@@ -0,0 +1 @@
* Refactored app to use OpenAPI SDK to improve performance and project structure.

View File

@@ -0,0 +1 @@
* Added other languages to app

View File

@@ -0,0 +1 @@
* Added French, Danish, Spanish, French, Japanese, Polish, and Finish translation to the app

View File

@@ -5,14 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000318"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000204">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: upload_to_play_store" time="111.253169"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="11.673502">
<failure message="/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:22:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:109:in `block (2 levels) in run&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:353:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:42:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/cli_tools_distributor.rb:122:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.204.3/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Google Api Error: Invalid request - APK specifies a version code that has already been used." /> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="37.162935">
</testcase> </testcase>

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EKSKLUDERET",
"album_info_card_backup_album_included": "INKLUDERET",
"album_viewer_appbar_share_delete": "Slet album",
"album_viewer_appbar_share_err_delete": "Fejlede sletning af album",
"album_viewer_appbar_share_err_leave": "Fejlede i at forlade album",
"album_viewer_appbar_share_err_remove": "Der er problemer med at fjerne elementer fra album",
"album_viewer_appbar_share_err_title": "Fejlede i at ændre albumtitel",
"album_viewer_appbar_share_leave": "Forlad album",
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Tilføj brugere",
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
"backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere",
"backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.",
"backup_album_selection_page_select_albums": "Vælg albummer",
"backup_album_selection_page_selection_info": "Oplysninger om valgte",
"backup_album_selection_page_total_assets": "Samlede unikke elementer",
"backup_all": "Alt",
"backup_controller_page_albums": "Sikkerhedskopier albummer",
"backup_controller_page_backup": "Sikkerhedskopier",
"backup_controller_page_backup_selected": "Valgte: ",
"backup_controller_page_backup_sub": "Sikkerhedskopierede billeder og videoer",
"backup_controller_page_cancel": "Annuller",
"backup_controller_page_created": "Oprettet den: {}",
"backup_controller_page_desc_backup": "Slå sikkerhedskopiering til automatisk at uploade nye elementer til serveren.",
"backup_controller_page_excluded": "Ekskluderet: ",
"backup_controller_page_failed": "Felet ({})",
"backup_controller_page_filename": "Filnavn: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Sikkerhedskopieringsinformation",
"backup_controller_page_none_selected": "Ingen valgte",
"backup_controller_page_remainder": "Tilbageværende",
"backup_controller_page_remainder_sub": "Tilbageværende billeder og albummer, at sikkerhedskopiere, fra valgte",
"backup_controller_page_select": "Vælg",
"backup_controller_page_server_storage": "Serverlager",
"backup_controller_page_start_backup": "Start sikkerhedskopiering",
"backup_controller_page_status_off": "Sikkerhedskopiering er slået fra",
"backup_controller_page_status_on": "Sikkerhedskopiering er slået til",
"backup_controller_page_storage_format": "{} af {} brugt",
"backup_controller_page_to_backup": "Albummer at sikkerhedskopiere",
"backup_controller_page_total": "I alt",
"backup_controller_page_total_sub": "Alle unikke billeder og videoer fra valgte albummer",
"backup_controller_page_turn_off": "Slå sikkerhedskopiering fra",
"backup_controller_page_turn_on": "Slå sikkerhedskopiering til",
"backup_controller_page_uploading_file_info": "Uploader filinformation",
"backup_err_only_album": "Kan ikke slette det eneste album",
"backup_info_card_assets": "elementer",
"control_bottom_app_bar_delete": "Slet",
"create_shared_album_page_share": "Del",
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
"create_shared_album_page_share_select_photos": "Vælg billeder",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E d. LLL y • hh:mm",
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
"delete_dialog_cancel": "Annuller",
"delete_dialog_ok": "Slet",
"delete_dialog_title": "Slet permanent",
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "LOKATION",
"login_form_button_text": "Log ind",
"login_form_email_hint": "din-email@email.com",
"login_form_endpoint_hint": "http://din-server-ip:port/api",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Angiv venligst http:// eller https://",
"login_form_err_invalid_email": "Ugyldig email",
"login_form_err_leading_whitespace": "Mellemrum før",
"login_form_err_trailing_whitespace": "Mellemrum efter",
"login_form_failed_login": "Der opstod en vejl ved at logge ind. Tjek server URL, email og kodeordet",
"login_form_label_email": "Email",
"login_form_label_password": "Kodeord",
"login_form_password_hint": "kodeord",
"login_form_save_login": "Forbliv logget ind",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
"profile_drawer_sign_out": "Log ud",
"search_bar_hint": "Søg i dine billeder",
"search_page_no_objects": "Ingen elementer er tilgængelige",
"search_page_no_places": "Ingen placeringsinformation er tilgængelig",
"search_page_places": "Steder",
"search_page_things": "Ting",
"search_result_page_new_search_hint": "Ny søgning",
"select_additional_user_for_sharing_page_suggestions": "Anbefalinger",
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
"select_user_for_sharing_page_share_suggestions": "Anbefalinger",
"share_add": "Tilføj",
"share_add_photos": "Tilføj billeder",
"share_add_title": "Tilføj en titel",
"share_create_album": "Opret album",
"share_invite": "Inviter til album",
"sharing_page_album": "Delt albums",
"sharing_page_description": "Opret delte albummer for at dele billeder og video med personer på dit netværk.",
"sharing_page_empty_list": "TOM LISTE",
"sharing_silver_appbar_create_shared_album": "Opret delt album",
"sharing_silver_appbar_share_partner": "Del med partner",
"tab_controller_nav_photos": "Billeder",
"tab_controller_nav_search": "Søg",
"tab_controller_nav_sharing": "Deling",
"version_announcement_overlay_ack": "Vedkend",
"version_announcement_overlay_release_notes": "udgivelsesnoter",
"version_announcement_overlay_text_1": "Hej vej, der er en ny version af",
"version_announcement_overlay_text_2": "bresøg venligst ",
"version_announcement_overlay_text_3": " og sikker dig, at din dockercompose og .env-fil er opdateret, for at undgå fejlkonfiguration, specielt hvis u bruger WatchTowereller andre mekanisme, der automatisk opdaterer serverprogrammer.",
"version_announcement_overlay_title": "Ny serverversion er tilgængelig \uD83C\uDF89"
}

View File

@@ -67,6 +67,7 @@
"login_form_err_invalid_email": "Ungültige E-Mail", "login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen", "login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen", "login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "E-Mail", "login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort", "login_form_label_password": "Passwort",
"login_form_password_hint": "password", "login_form_password_hint": "password",
@@ -75,12 +76,14 @@
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell", "profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_sign_out": "Abmelden", "profile_drawer_sign_out": "Abmelden",
"search_bar_hint": "Durchsuche deine Fotos", "search_bar_hint": "Durchsuche deine Fotos",
"search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar", "search_page_no_places": "Keine Informationen über Orte verfügbar",
"search_page_places": "Orte", "search_page_places": "Orte",
"search_page_things": "Dinge", "search_page_things": "Dinge",
"search_result_page_new_search_hint": "Neue Suche", "search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge", "select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"share_add": "Hinzufügen", "share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen", "share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen", "share_add_title": "Titel hinzufügen",

View File

@@ -30,7 +30,7 @@
"backup_controller_page_info": "Backup Information", "backup_controller_page_info": "Backup Information",
"backup_controller_page_none_selected": "None selected", "backup_controller_page_none_selected": "None selected",
"backup_controller_page_remainder": "Remainder", "backup_controller_page_remainder": "Remainder",
"backup_controller_page_remainder_sub": "Remaining photos and albums to back up from selection", "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
"backup_controller_page_select": "Select", "backup_controller_page_select": "Select",
"backup_controller_page_server_storage": "Server Storage", "backup_controller_page_server_storage": "Server Storage",
"backup_controller_page_start_backup": "Start Backup", "backup_controller_page_start_backup": "Start Backup",
@@ -67,15 +67,16 @@
"login_form_err_invalid_email": "Invalid Email", "login_form_err_invalid_email": "Invalid Email",
"login_form_err_leading_whitespace": "Leading whitespace", "login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace", "login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "Email", "login_form_label_email": "Email",
"login_form_label_password": "Password", "login_form_label_password": "Password",
"login_form_password_hint": "password", "login_form_password_hint": "password",
"login_form_save_login": "Stay logged in", "login_form_save_login": "Stay logged in",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_sign_out": "Sign Out", "profile_drawer_sign_out": "Sign Out",
"search_bar_hint": "Search your photos", "search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available", "search_page_no_places": "No Places Info Available",
"search_page_places": "Places", "search_page_places": "Places",
"search_page_things": "Things", "search_page_things": "Things",

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS",
"album_viewer_appbar_share_delete": "Eliminar álbum ",
"album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum",
"album_viewer_appbar_share_err_leave": "No ha podido dejar el álbum",
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los activos del álbum",
"album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ",
"album_viewer_appbar_share_leave": "Abandonar álbum ",
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
"album_viewer_page_share_add_users": "Añadir usuarios",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
"backup_album_selection_page_assets_scatter": "Los activos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
"backup_album_selection_page_select_albums": "Seleccionar Álbumes",
"backup_album_selection_page_selection_info": "Información sobre la Selección",
"backup_album_selection_page_total_assets": "Total de activos únicos",
"backup_all": "Todos",
"backup_controller_page_albums": "Álbumes de copia de seguridad",
"backup_controller_page_backup": "Copia de Seguridad",
"backup_controller_page_backup_selected": "Seleccionado:",
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
"backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "",
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
"backup_controller_page_excluded": "Excluido:",
"backup_controller_page_failed": "",
"backup_controller_page_filename": "",
"backup_controller_page_id": "",
"backup_controller_page_info": "Información de la Copia de Seguridad",
"backup_controller_page_none_selected": "Ninguno seleccionado",
"backup_controller_page_remainder": "Remanente",
"backup_controller_page_remainder_sub": "Fotos y álbumes restantes para hacer una copia de seguridad de la selección",
"backup_controller_page_select": "Seleccionar",
"backup_controller_page_server_storage": "Almacenamiento en el servidor",
"backup_controller_page_start_backup": "Iniciar copia de seguridad",
"backup_controller_page_status_off": "La copia de seguridad está desactivada",
"backup_controller_page_status_on": "La copia de seguridad está activada",
"backup_controller_page_storage_format": "{} de {} usadas",
"backup_controller_page_to_backup": "Álbumes a respaldar",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
"backup_controller_page_turn_on": "Activar la copia de seguridad",
"backup_controller_page_uploading_file_info": "",
"backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "activos",
"control_bottom_app_bar_delete": "Eliminar",
"create_shared_album_page_share": "Compartir",
"create_shared_album_page_share_add_assets": "AÑADIR ACTIVOS",
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
"daily_title_text_date": "E dd, MMM",
"daily_title_text_date_year": "E dd de MMM, yyyy",
"date_format": "E d, LLL y • h:mm a",
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar",
"delete_dialog_title": "Eliminar Permanentemente",
"exif_bottom_sheet_description": "Añadir Descripción...",
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "LOCALZACIÓN",
"login_form_button_text": "Iniciar Sesión",
"login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
"login_form_endpoint_url": "URL del servidor",
"login_form_err_http": "Por favor, especifique http:// o https://",
"login_form_err_invalid_email": "Correo electrónico no válido",
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_login": "",
"login_form_label_email": "Correo",
"login_form_label_password": "Contraseña",
"login_form_password_hint": "contraseña",
"login_form_save_login": "Mantener la sesión iniciada",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_sign_out": "Cerrar Sesión",
"search_bar_hint": "Busca tus fotos",
"search_page_no_objects": "",
"search_page_no_places": "No hay información de lugares disponibles",
"search_page_places": "Lugares",
"search_page_things": "Cosas",
"search_result_page_new_search_hint": "Nueva Busqueda",
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
"select_user_for_sharing_page_share_suggestions": "",
"share_add": "Añadir",
"share_add_photos": "Añadir fotos",
"share_add_title": "Añadir un título",
"share_create_album": "Crear álbum",
"share_invite": "Invitar al álbum",
"sharing_page_album": "Álbumes compartidos",
"sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.",
"sharing_page_empty_list": "LISTA VACIA",
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
"sharing_silver_appbar_share_partner": "Compartir con el compañero",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Buscar",
"tab_controller_nav_sharing": "Compartiendo",
"version_announcement_overlay_ack": "Reconocer",
"version_announcement_overlay_release_notes": "notas de versión",
"version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de",
"version_announcement_overlay_text_2": "tómese su tiempo para visitar la ",
"version_announcement_overlay_text_3": "y asegurate de que tu configuración de docker-compose y .env está actualizada para evitar cualquier desconfiguración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su aplicación de servidor automáticamente.",
"version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "JÄTETTY POIS",
"album_info_card_backup_album_included": "SISÄLLYTETTY",
"album_viewer_appbar_share_delete": "Poista albumi",
"album_viewer_appbar_share_err_delete": "Albumin poistaminen epäonnistui",
"album_viewer_appbar_share_err_leave": "Albumista poistuminen epäonnistui",
"album_viewer_appbar_share_err_remove": "Ongelmia kohteiden poistamisessa albumista",
"album_viewer_appbar_share_err_title": "Albumin nimen muuttaminen epäonnistui",
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",
"backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois",
"backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.",
"backup_album_selection_page_select_albums": "Valitse albumit",
"backup_album_selection_page_selection_info": "Valintatiedot",
"backup_album_selection_page_total_assets": "Uniikkeja kohteita yhteensä",
"backup_all": "Kaikki",
"backup_controller_page_albums": "Varmuuskopioi albumit",
"backup_controller_page_backup": "Varmuuskopioitu",
"backup_controller_page_backup_selected": "Valittu:",
"backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot",
"backup_controller_page_cancel": "Peruuta",
"backup_controller_page_created": "Luotu: {}",
"backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle ladataksesi uudet kohteet palvelimelle automaattisesti.",
"backup_controller_page_excluded": "Jätetty pois:",
"backup_controller_page_failed": "Epäonnistui ({})",
"backup_controller_page_filename": "Tiedoston nimi: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Varmuuskopioinnin tiedot",
"backup_controller_page_none_selected": "Ei mitään",
"backup_controller_page_remainder": "Jäljellä",
"backup_controller_page_remainder_sub": "Varmuuskopiointia odottavat kuvat ja videot",
"backup_controller_page_select": "Valitse",
"backup_controller_page_server_storage": "Palvelimen tallennustila",
"backup_controller_page_start_backup": "Aloita varmuuskopiointi",
"backup_controller_page_status_off": "Varmuuskopiointi on pois päältä",
"backup_controller_page_status_on": "Varmuuskopiointi on päällä",
"backup_controller_page_storage_format": "{} / {} käytetty",
"backup_controller_page_to_backup": "Varmuuskopioitavat albumit",
"backup_controller_page_total": "Yhteensä",
"backup_controller_page_total_sub": "Kaikki uniikit kuvat ja videot valituista albumeista",
"backup_controller_page_turn_off": "Varmuuskopiointi pois päältä",
"backup_controller_page_turn_on": "Varmuuskopiointi päälle",
"backup_controller_page_uploading_file_info": "Tiedostojen lähetystiedot",
"backup_err_only_album": "Vähintään yhden albumin tulee olla valittuna",
"backup_info_card_assets": "kohdetta",
"control_bottom_app_bar_delete": "Poista",
"create_shared_album_page_share": "Jaa",
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
"create_shared_album_page_share_select_photos": "Valitse kuvat",
"daily_title_text_date": "",
"daily_title_text_date_year": "",
"date_format": "",
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
"delete_dialog_cancel": "Peruuta",
"delete_dialog_ok": "Poista",
"delete_dialog_title": "Poista pysyvästi",
"exif_bottom_sheet_description": "Lisää kuvaus…",
"exif_bottom_sheet_details": "TIEDOT",
"exif_bottom_sheet_location": "SIJAINTI",
"login_form_button_text": "Kirjaudu",
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
"login_form_endpoint_url": "Palvelimen URL",
"login_form_err_http": "Lisää http:// tai https://",
"login_form_err_invalid_email": "Virheellinen sähköpostiosoite",
"login_form_err_leading_whitespace": "Alussa välilyönti",
"login_form_err_trailing_whitespace": "Lopussa välilyönti",
"login_form_failed_login": "Virhe kirjautumisessa. Tarkista palvelimen URL, sähköpostiosoite ja salasana.",
"login_form_label_email": "Sähköposti",
"login_form_label_password": "Salasana",
"login_form_password_hint": "salasana",
"login_form_save_login": "Pysy kirjautuneena",
"monthly_title_text_date_format": "",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_sign_out": "Kirjaudu ulos",
"search_bar_hint": "Etsi kuvia",
"search_page_no_objects": "Objektitietoja ei ole saatavilla",
"search_page_no_places": "Paikkatietoja ei ole saatavilla",
"search_page_places": "Paikat",
"search_page_things": "Asiat",
"search_result_page_new_search_hint": "Uusi haku",
"select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
"select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
"select_user_for_sharing_page_share_suggestions": "Ehdotukset",
"share_add": "Lisää",
"share_add_photos": "Lisää kuvia",
"share_add_title": "Lisää nimi",
"share_create_album": "Luo albumi",
"share_invite": "Kutsu albumiin",
"sharing_page_album": "Jaetut albumit",
"sharing_page_description": "Luo jaettuja albumeja jakaaksesi kuvia ja videoita läheisillesi.",
"sharing_page_empty_list": "TYHJÄ LISTA",
"sharing_silver_appbar_create_shared_album": "Luo jaettu albumi",
"sharing_silver_appbar_share_partner": "Jaa kumppanille",
"tab_controller_nav_photos": "Kuvat",
"tab_controller_nav_search": "Haku",
"tab_controller_nav_sharing": "Jakaminen",
"version_announcement_overlay_ack": "Tiedostan",
"version_announcement_overlay_release_notes": "julkaisutiedoissa",
"version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta",
"version_announcement_overlay_text_2": "Ota hetki aikaa vieraillaksesi",
"version_announcement_overlay_text_3": "ja varmista, että käyttämäsi docker-compose ja .env-asetukset ovat ajantasalla välttyäksesi asetusongelmilta. Varsinkin jos käytät WatchToweria tai jotain muuta mekanismia päivittääksesi palvelinsovellusta automaattisesti.",
"version_announcement_overlay_title": "Uusi palvelinversio saatavilla \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
"album_viewer_appbar_share_err_remove": "Il y a des problèmes pour retirer les éléments de l'album",
"album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album",
"album_viewer_appbar_share_leave": "Quitter l'album",
"album_viewer_appbar_share_remove": "Retirer de l'album",
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
"backup_album_selection_page_select_albums": "Sélectionner les albums",
"backup_album_selection_page_selection_info": "Informations sur la sélection",
"backup_album_selection_page_total_assets": "Total des éléments uniques",
"backup_all": "Tout",
"backup_controller_page_albums": "Sauvegarder les albums",
"backup_controller_page_backup": "Sauvegardé",
"backup_controller_page_backup_selected": "Sélectionné : ",
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
"backup_controller_page_cancel": "Annuler",
"backup_controller_page_created": "Créé le : {}",
"backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.",
"backup_controller_page_excluded": "Exclus : ",
"backup_controller_page_failed": "Échec de l'opération ({})",
"backup_controller_page_filename": "Nom du fichier : {} [{}]",
"backup_controller_page_id": "ID : {}",
"backup_controller_page_info": "Informations de sauvegarde",
"backup_controller_page_none_selected": "Aucune sélection",
"backup_controller_page_remainder": "Restant",
"backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection",
"backup_controller_page_select": "Sélectionner",
"backup_controller_page_server_storage": "Stockage du serveur",
"backup_controller_page_start_backup": "Démarrer la sauvegarde",
"backup_controller_page_status_off": "La sauvegarde est désactivée",
"backup_controller_page_status_on": "La sauvegarde est activée",
"backup_controller_page_storage_format": "{} de {} utilisé",
"backup_controller_page_to_backup": "Albums à sauvegarder",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
"backup_controller_page_turn_off": "Désactiver la sauvegarde",
"backup_controller_page_turn_on": "Activer la sauvegarde",
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"control_bottom_app_bar_delete": "Supprimer",
"create_shared_album_page_share": "Partager",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
"delete_dialog_cancel": "Annuler",
"delete_dialog_ok": "Supprimer",
"delete_dialog_title": "Supprimer définitivement",
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
"login_form_endpoint_url": "URL du point d'accès au serveur",
"login_form_err_http": "Veuillez préciser http:// ou https://",
"login_form_err_invalid_email": "Email invalide",
"login_form_err_leading_whitespace": "Espace en début de ligne",
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
"login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe",
"login_form_label_email": "Email",
"login_form_label_password": "Mot de passe",
"login_form_password_hint": "mot de passe",
"login_form_save_login": "Rester connecté",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_sign_out": "Se déconnecter",
"search_bar_hint": "Rechercher vos photos",
"search_page_no_objects": "Aucune information disponible sur les objets",
"search_page_no_places": "Aucune information disponible sur la localisation",
"search_page_places": "Lieux",
"search_page_things": "Objets",
"search_result_page_new_search_hint": "Nouvelle recherche",
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"share_add": "Ajouter",
"share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre",
"share_create_album": "Créer un album",
"share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés",
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
"sharing_page_empty_list": "LISTE VIDE",
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Recherche",
"tab_controller_nav_sharing": "Partage",
"version_announcement_overlay_ack": "Confirmer",
"version_announcement_overlay_release_notes": "notes de mise à jour",
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
"version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
"version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_viewer_appbar_share_delete": "Elimina album ",
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia l'album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"backup_album_selection_page_albums_device": "Albums nel device ({})",
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
"backup_album_selection_page_select_albums": "Seleziona gli album",
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
"backup_all": "Tutti",
"backup_controller_page_albums": "Backup album",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selezionati:",
"backup_controller_page_backup_sub": "Photo e video salvati",
"backup_controller_page_cancel": "Cancella ",
"backup_controller_page_created": "Creato il: {}",
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
"backup_controller_page_excluded": "Esclusi:",
"backup_controller_page_failed": "Falliti: ({})",
"backup_controller_page_filename": "Nome del file: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informazioni sul backup",
"backup_controller_page_none_selected": "Nessuna selezione",
"backup_controller_page_remainder": "Promemoria ",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
"backup_controller_page_select": "Seleziona ",
"backup_controller_page_server_storage": "Spazio nel server",
"backup_controller_page_start_backup": "Inizia backup ",
"backup_controller_page_status_off": "Backup è disattivato ",
"backup_controller_page_status_on": "Backup è attivato",
"backup_controller_page_storage_format": "{} di {} usati",
"backup_controller_page_to_backup": "Album da salvare",
"backup_controller_page_total": "Totale",
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
"backup_controller_page_turn_off": "Disattiva backup",
"backup_controller_page_turn_on": "Attiva backup ",
"backup_controller_page_uploading_file_info": "Info sul file caricato",
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
"backup_info_card_assets": "Oggetti ",
"control_bottom_app_bar_delete": "Elimina",
"create_shared_album_page_share": "Condividi",
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
"create_shared_album_page_share_select_photos": "Seleziona foto",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, d LLL, y • hh:mm",
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_title": "Cancella in modo permanente ",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
"login_form_button_text": "Accedi",
"login_form_email_hint": "tuaemail@email.com",
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
"login_form_endpoint_url": "URL del Server Endpoint",
"login_form_err_http": "Per favore specificare http:// o https://",
"login_form_err_invalid_email": "Email non valida",
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
"login_form_err_trailing_whitespace": "Spazio bianco alla fine",
"login_form_failed_login": "Errore nel login, controlla URL del server e le credenziali (email e password)",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_password_hint": "password ",
"login_form_save_login": "Rimani connesso ",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_sign_out": "Esci",
"search_bar_hint": "Cerca le tue foto",
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
"search_page_no_places": "Nessun informazione sulla posizione ",
"search_page_places": "Luoghi",
"search_page_things": "Oggetti",
"search_result_page_new_search_hint": "Nuova ricerca ",
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
"share_add": "Aggiungi",
"share_add_photos": "Aggiungi foto",
"share_add_title": "Aggiungi un titolo ",
"share_create_album": "Crea album",
"share_invite": "Invitare all'album ",
"sharing_page_album": "Album condivisi",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
"sharing_page_empty_list": "LISTA VUOTA",
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
"sharing_silver_appbar_share_partner": "Condividi con il partner",
"tab_controller_nav_photos": "Foto",
"tab_controller_nav_search": "Cerca",
"tab_controller_nav_sharing": "Condividi",
"version_announcement_overlay_ack": "Riconosci ",
"version_announcement_overlay_release_notes": "le note di rilascio ",
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
"version_announcement_overlay_text_2": "prova a controllare ",
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "除外",
"album_info_card_backup_album_included": "選択",
"album_viewer_appbar_share_delete": "アルバムを削除",
"album_viewer_appbar_share_err_delete": "削除に失敗...",
"album_viewer_appbar_share_err_leave": "退会に失敗...",
"album_viewer_appbar_share_err_remove": "アルバムから写真を除外する際にエラー発生",
"album_viewer_appbar_share_err_title": "タイトルの変更に失敗...",
"album_viewer_appbar_share_leave": "アルバムから退会",
"album_viewer_appbar_share_remove": "アルバムから除外",
"album_viewer_page_share_add_users": "ユーザーを追加",
"backup_album_selection_page_albums_device": "端末上のアルバム数は {} だよ",
"backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外だよ",
"backup_album_selection_page_assets_scatter": "写真がいろんなアルバムに登録されてる事があるから、アルバムを含めたり除外したりしてどの写真を保存するか選択できるよ。",
"backup_album_selection_page_select_albums": "アルバムを選択",
"backup_album_selection_page_selection_info": "選択、又は除外されてるアルバム",
"backup_album_selection_page_total_assets": "選択されたアルバムの写真と動画の数",
"backup_all": "全て",
"backup_controller_page_albums": "アルバム",
"backup_controller_page_backup": "バックアップ",
"backup_controller_page_backup_selected": "選択されてる:",
"backup_controller_page_backup_sub": "バックアップされた写真と動画の数だよ",
"backup_controller_page_cancel": "キャンセルするよ",
"backup_controller_page_created": "{} に作成されたよ",
"backup_controller_page_desc_backup": "ONにすれば自動的に新しい写真などがバックアップされるようになるよ",
"backup_controller_page_excluded": "除外されてるアルバム:",
"backup_controller_page_failed": "失敗: ({})",
"backup_controller_page_filename": "ファイル名: {} [{}] ",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "バックアップ情報",
"backup_controller_page_none_selected": "何も選んでないよ",
"backup_controller_page_remainder": "リマインダー",
"backup_controller_page_remainder_sub": "残りの写真と動画の数だよ",
"backup_controller_page_select": "選択",
"backup_controller_page_server_storage": "サーバーの容量",
"backup_controller_page_start_backup": "バックアップを開始するよ",
"backup_controller_page_status_off": "バックアップがOFFになってるよ",
"backup_controller_page_status_on": "バックアップがONになってるよ",
"backup_controller_page_storage_format": "{}中、 {}を使用中だよ",
"backup_controller_page_to_backup": "バックアップされるアルバム",
"backup_controller_page_total": "トータル",
"backup_controller_page_total_sub": "選択されたアルバムの写真と動画の数だよ",
"backup_controller_page_turn_off": "バックアップOFF",
"backup_controller_page_turn_on": "バックアップON",
"backup_controller_page_uploading_file_info": "アップロードされてるファイルに関する情報",
"backup_err_only_album": "唯一のアルバムを除外する事はできないよ",
"backup_info_card_assets": "写真と動画",
"control_bottom_app_bar_delete": "削除",
"create_shared_album_page_share": "共有",
"create_shared_album_page_share_add_assets": "写真や動画を追加",
"create_shared_album_page_share_select_photos": "写真を選択",
"daily_title_text_date": "E, MM月 dd日",
"daily_title_text_date_year": "E, yyyy年 MM月 dd日",
"date_format": "E, MM月 dd日 • hh時mm分",
"delete_dialog_alert": "サーバーからも端末からも永久的に削除されるけど良いの?",
"delete_dialog_cancel": "キャンセル",
"delete_dialog_ok": "削除",
"delete_dialog_title": "永久的に削除",
"exif_bottom_sheet_description": "概要を追加",
"exif_bottom_sheet_details": "詳細な情報",
"exif_bottom_sheet_location": "撮影地",
"login_form_button_text": "ログイン",
"login_form_email_hint": "example@email.com",
"login_form_endpoint_hint": "https://example.com:port/api",
"login_form_endpoint_url": "サーバーエンドポイントURL",
"login_form_err_http": "http://かhttps://かを指定してね",
"login_form_err_invalid_email": "メールアドレスが有効じゃないよ",
"login_form_err_leading_whitespace": "最初に半角スペースが含まれてるよ",
"login_form_err_trailing_whitespace": "最後に半角スペースが含まれてるよ",
"login_form_failed_login": "ログインエラー。サーバーのURL、メールアドレスとパスワードを再確認してね",
"login_form_label_email": "メールアドレス",
"login_form_label_password": "パスワード",
"login_form_password_hint": "パスワード",
"login_form_save_login": "ログインしたままにする",
"monthly_title_text_date_format": "yyyy年 MM月",
"profile_drawer_client_server_up_to_date": "サーバーとクライアント、両方最新バージョンだよ",
"profile_drawer_sign_out": "サインアウト",
"search_bar_hint": "写真を検索",
"search_page_no_objects": "被写体に関するデータがないよ",
"search_page_no_places": "場所に関するデータがないよ",
"search_page_places": "撮影地",
"search_page_things": "カテゴリ",
"search_result_page_new_search_hint": "検索",
"select_additional_user_for_sharing_page_suggestions": "ユーザーリスト",
"select_user_for_sharing_page_err_album": "アルバム作成に失敗...",
"select_user_for_sharing_page_share_suggestions": "ユーザーの一覧だよ",
"share_add": "追加",
"share_add_photos": "写真を追加",
"share_add_title": "タイトルを追加",
"share_create_album": "アルバムを作成",
"share_invite": "アルバムに参加",
"sharing_page_album": "共有アルバム",
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる仲間に写真を共有してみよう!",
"sharing_page_empty_list": "誰も居ないね ( T_T)(^-^ ) ドンマイ",
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
"sharing_silver_appbar_share_partner": "パートナーと共有",
"tab_controller_nav_photos": "写真",
"tab_controller_nav_search": "検索",
"tab_controller_nav_sharing": "共有",
"version_announcement_overlay_ack": "了解",
"version_announcement_overlay_release_notes": "更新情報",
"version_announcement_overlay_text_1": "こんにちは、又はこんばんは!新しい",
"version_announcement_overlay_text_2": "のバージョンが公開中だよ。",
"version_announcement_overlay_text_3": "を確認してみてね。あと、docker-composeや.envファイルが最新の状態に更新されてか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してね",
"version_announcement_overlay_title": "新しいバージョン、公開中\uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "WYKLUCZONE",
"album_info_card_backup_album_included": "WŁĄCZONE",
"album_viewer_appbar_share_delete": "Usuń album",
"album_viewer_appbar_share_err_delete": "Nie udało się usunąć albumu",
"album_viewer_appbar_share_err_leave": "Nie udało się wyjść z albumu",
"album_viewer_appbar_share_err_remove": "Wystąpiły problemy z usunięciem plików z albumu",
"album_viewer_appbar_share_err_title": "Nie udało się zmienić tytułu albumu",
"album_viewer_appbar_share_leave": "Opuść album",
"album_viewer_appbar_share_remove": "Usuń z albumu",
"album_viewer_page_share_add_users": "Dodaj użytkowników",
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
"backup_album_selection_page_select_albums": "Zaznacz albumy",
"backup_album_selection_page_selection_info": "Info o wyborze",
"backup_album_selection_page_total_assets": "Łącznie unikalnych plików",
"backup_all": "Wszystkie",
"backup_controller_page_albums": "Backup Albumów",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Zaznaczone: ",
"backup_controller_page_backup_sub": "Tworzenie kopii zapasowych zdjęć i filmów",
"backup_controller_page_cancel": "Anuluj",
"backup_controller_page_created": "Utworzony na: {}",
"backup_controller_page_desc_backup": "Włącz backup, aby automatycznie przesyłać nowe zasoby na serwer.",
"backup_controller_page_excluded": "Wykluczone: ",
"backup_controller_page_failed": "Nieudane ({})",
"backup_controller_page_filename": "Nazwa pliku: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informacje o kopii zapasowej",
"backup_controller_page_none_selected": "Brak wybranych",
"backup_controller_page_remainder": "Reszta",
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru",
"backup_controller_page_select": "Zaznacz",
"backup_controller_page_server_storage": "Pamięć Serwera",
"backup_controller_page_start_backup": "Rozpocznij Backup",
"backup_controller_page_status_off": "Backup jest wyłączony",
"backup_controller_page_status_on": "Backup jest włączony",
"backup_controller_page_storage_format": "{} z {} wykorzystanych",
"backup_controller_page_to_backup": "Albumy do backupu",
"backup_controller_page_total": "Łącznie",
"backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów",
"backup_controller_page_turn_off": "Wyłącz Backup",
"backup_controller_page_turn_on": "Włącz Backup",
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
"backup_err_only_album": "Nie można usunąć tylko i wyłącznie albumu",
"backup_info_card_assets": "pliki",
"control_bottom_app_bar_delete": "Usuń",
"create_shared_album_page_share": "Udostępnij",
"create_shared_album_page_share_add_assets": "DODAJ PLIKI",
"create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia",
"delete_dialog_cancel": "Anuluj",
"delete_dialog_ok": "Usuń",
"delete_dialog_title": "Usuń trwale",
"exif_bottom_sheet_description": "Dodaj opis...",
"exif_bottom_sheet_details": "SZCZEGÓŁY",
"exif_bottom_sheet_location": "LOKALIZACJA",
"login_form_button_text": "Login",
"login_form_email_hint": "twojmail@email.com",
"login_form_endpoint_hint": "http://ip-twojego-serwera:port/api",
"login_form_endpoint_url": "URL Serwera",
"login_form_err_http": "Proszę określić http:// lub https://",
"login_form_err_invalid_email": "Niepoprawny emaill",
"login_form_err_leading_whitespace": "Białe znaki",
"login_form_err_trailing_whitespace": "Białe znaki po przecinku",
"login_form_failed_login": "Błąd logowania, sprawdź adres url serwera, email i hasło.",
"login_form_label_email": "Email",
"login_form_label_password": "Hasło",
"login_form_password_hint": "hasło",
"login_form_save_login": "Pozostań zalogowany",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
"profile_drawer_sign_out": "Wyloguj się",
"search_bar_hint": "Szukaj swoich zdjęć",
"search_page_no_objects": "Brak informacji o obiektach",
"search_page_no_places": "Brak informacji o miejscu",
"search_page_places": "Miejsca",
"search_page_things": "Rzeczy",
"search_result_page_new_search_hint": "Nowe wyszukiwanie",
"select_additional_user_for_sharing_page_suggestions": "Propozycje",
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
"select_user_for_sharing_page_share_suggestions": "Propozycje",
"share_add": "Dodaj",
"share_add_photos": "Dodaj zdjęcia",
"share_add_title": "Dodaj tytuł",
"share_create_album": "Utwórz album",
"share_invite": "Zaproś do albumu",
"sharing_page_album": "Udostępnione albumy",
"sharing_page_description": "Twórz wspóldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.",
"sharing_page_empty_list": "PUSTA LISTA",
"sharing_silver_appbar_create_shared_album": "Utwórz współdzielony album",
"sharing_silver_appbar_share_partner": "Udostępnij partnerce/partnerowi",
"tab_controller_nav_photos": "Zdjęcia",
"tab_controller_nav_search": "Szukaj",
"tab_controller_nav_sharing": "Udostępnianie",
"version_announcement_overlay_ack": "Potwierdzenie",
"version_announcement_overlay_release_notes": "informacje o wydaniu",
"version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie",
"version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ",
"version_announcement_overlay_text_3": " i upewnij się, że twoja konfiguracja docker-compose i .env jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczną aktualizację aplikacji serwera.",
"version_announcement_overlay_title": "Nowa wersja serwera dostępna \uD83C\uDF89"
}

View File

@@ -19,6 +19,8 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- shared_preferences_ios (0.0.1):
- Flutter
- sqflite (0.0.2): - sqflite (0.0.2):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
@@ -38,6 +40,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
@@ -64,6 +67,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
photo_manager: photo_manager:
:path: ".symlinks/plugins/photo_manager/ios" :path: ".symlinks/plugins/photo_manager/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios: url_launcher_ios:
@@ -83,6 +88,7 @@ SPEC CHECKSUMS:
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 35;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 35;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 35;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.10.0</string> <string>1.18.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>14</string> <string>35</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -87,6 +87,13 @@
<array> <array>
<string>en</string> <string>en</string>
<string>de</string> <string>de</string>
<string>da</string>
<string>es</string>
<string>fr</string>
<string>it</string>
<string>fi</string>
<string>ja</string>
<string>pl</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

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

View File

@@ -5,12 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.526426">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.096281">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.476898">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="102.893162">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="130.468341">
</testcase> </testcase>

View File

@@ -43,7 +43,11 @@ void main() async {
// Default locale // Default locale
Locale('en', 'US'), Locale('en', 'US'),
// Additional locales // Additional locales
Locale('de', 'DE') Locale('da', 'DK'),
Locale('de', 'DE'),
Locale('es', 'ES'),
Locale('fr', 'FR'),
Locale('it', 'IT'),
]; ];
runApp( runApp(

View File

@@ -140,7 +140,7 @@ class SearchPage extends HookConsumerWidget {
return ThumbnailWithInfo( return ThumbnailWithInfo(
imageUrl: imageUrl:
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60', 'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
textInfo: 'sarch_no_objects'.tr(), textInfo: 'search_page_no_objects'.tr(),
onTap: () {}, onTap: () {},
); );
}), }),

View File

@@ -134,7 +134,6 @@ class SharedAlbumService {
await _apiService.albumApi.updateAlbumInfo( await _apiService.albumApi.updateAlbumInfo(
albumId, albumId,
UpdateAlbumDto( UpdateAlbumDto(
ownerId: ownerId,
albumName: newAlbumTitle, albumName: newAlbumTitle,
), ),
); );

View File

@@ -96,13 +96,16 @@ class MonthGroupTitle extends HookConsumerWidget {
color: Colors.grey, color: Colors.grey,
), ),
), ),
Padding( GestureDetector(
padding: const EdgeInsets.only(left: 8.0), onTap: _handleTitleIconClick,
child: Text( child: Padding(
_getSimplifiedMonth(), padding: const EdgeInsets.only(left: 8.0),
style: TextStyle( child: Text(
fontSize: 24, _getSimplifiedMonth(),
color: Theme.of(context).primaryColor, style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
),
), ),
), ),
), ),

View File

@@ -26,17 +26,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(AssetResponseDto asset) { Widget _buildSelectionIcon(AssetResponseDto asset) {
if (selectedAsset.contains(asset) && !isAlbumExist) { var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Icon( return Icon(
Icons.check_circle, Icons.check_circle,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
); );
} else if (selectedAsset.contains(asset) && isAlbumExist) { } else if (isSelected && isAlbumExist) {
return const Icon( return const Icon(
Icons.check_circle, Icons.check_circle,
color: Color.fromARGB(255, 233, 233, 233), color: Color.fromARGB(255, 233, 233, 233),
); );
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) { } else if (isNewlySelected && isAlbumExist) {
return Icon( return Icon(
Icons.check_circle, Icons.check_circle,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
@@ -50,17 +54,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
} }
BoxBorder drawBorderColor() { BoxBorder drawBorderColor() {
if (selectedAsset.contains(asset) && !isAlbumExist) { var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Border.all( return Border.all(
color: Theme.of(context).primaryColorLight, color: Theme.of(context).primaryColorLight,
width: 10, width: 10,
); );
} else if (selectedAsset.contains(asset) && isAlbumExist) { } else if (isSelected && isAlbumExist) {
return Border.all( return Border.all(
color: const Color.fromARGB(255, 190, 190, 190), color: const Color.fromARGB(255, 190, 190, 190),
width: 10, width: 10,
); );
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) { } else if (isNewlySelected && isAlbumExist) {
return Border.all( return Border.all(
color: Theme.of(context).primaryColorLight, color: Theme.of(context).primaryColorLight,
width: 10, width: 10,
@@ -71,10 +79,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
var isSelected =
selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isAlbumExist) { if (isAlbumExist) {
// Operation for existing album // Operation for existing album
if (!selectedAsset.contains(asset)) { if (!isSelected) {
if (newAssetsForAlbum.contains(asset)) { if (isNewlySelected) {
ref ref
.watch(assetSelectionProvider.notifier) .watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets([asset]); .removeSelectedAdditionalAssets([asset]);
@@ -86,7 +99,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
} }
} else { } else {
// Operation for new album // Operation for new album
if (selectedAsset.contains(asset)) { if (isSelected) {
ref ref
.watch(assetSelectionProvider.notifier) .watch(assetSelectionProvider.notifier)
.removeSelectedNewAssets([asset]); .removeSelectedNewAssets([asset]);

View File

@@ -25,6 +25,6 @@ class ApiService {
} }
setAccessToken(String accessToken) { setAccessToken(String accessToken) {
_apiClient.addDefaultHeader('Authorization', 'bearer $accessToken'); _apiClient.addDefaultHeader('Authorization', 'Bearer $accessToken');
} }
} }

View File

@@ -29,6 +29,7 @@ doc/DeviceTypeEnum.md
doc/ExifResponseDto.md doc/ExifResponseDto.md
doc/LoginCredentialDto.md doc/LoginCredentialDto.md
doc/LoginResponseDto.md doc/LoginResponseDto.md
doc/LogoutResponseDto.md
doc/RemoveAssetsDto.md doc/RemoveAssetsDto.md
doc/SearchAssetDto.md doc/SearchAssetDto.md
doc/ServerInfoApi.md doc/ServerInfoApi.md
@@ -37,6 +38,7 @@ doc/ServerPingResponse.md
doc/ServerVersionReponseDto.md doc/ServerVersionReponseDto.md
doc/SignUpDto.md doc/SignUpDto.md
doc/SmartInfoResponseDto.md doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
@@ -83,6 +85,7 @@ lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart lib/model/exif_response_dto.dart
lib/model/login_credential_dto.dart lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
lib/model/remove_assets_dto.dart lib/model/remove_assets_dto.dart
lib/model/search_asset_dto.dart lib/model/search_asset_dto.dart
lib/model/server_info_response_dto.dart lib/model/server_info_response_dto.dart
@@ -90,6 +93,7 @@ lib/model/server_ping_response.dart
lib/model/server_version_reponse_dto.dart lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart
@@ -97,4 +101,3 @@ lib/model/user_count_response_dto.dart
lib/model/user_response_dto.dart lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart lib/model/validate_access_token_response_dto.dart
pubspec.yaml pubspec.yaml
test/validate_access_token_response_dto_test.dart

View File

@@ -89,6 +89,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info | *DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info |
*DeviceInfoApi* | [**updateDeviceInfo**](doc//DeviceInfoApi.md#updatedeviceinfo) | **PATCH** /device-info | *DeviceInfoApi* | [**updateDeviceInfo**](doc//DeviceInfoApi.md#updatedeviceinfo) | **PATCH** /device-info |
@@ -100,6 +101,7 @@ Class | Method | HTTP request | Description
*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | *UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user |
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | *UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | *UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count | *UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count |
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user | *UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user |
@@ -129,6 +131,7 @@ Class | Method | HTTP request | Description
- [ExifResponseDto](doc//ExifResponseDto.md) - [ExifResponseDto](doc//ExifResponseDto.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md) - [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
- [RemoveAssetsDto](doc//RemoveAssetsDto.md) - [RemoveAssetsDto](doc//RemoveAssetsDto.md)
- [SearchAssetDto](doc//SearchAssetDto.md) - [SearchAssetDto](doc//SearchAssetDto.md)
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md) - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
@@ -136,6 +139,7 @@ Class | Method | HTTP request | Description
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md) - [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
- [SignUpDto](doc//SignUpDto.md) - [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md) - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md) - [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md) - [UpdateUserDto](doc//UpdateUserDto.md)

View File

@@ -306,7 +306,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **removeAssetFromAlbum** # **removeAssetFromAlbum**
> removeAssetFromAlbum(albumId, removeAssetsDto) > AlbumResponseDto removeAssetFromAlbum(albumId, removeAssetsDto)
@@ -325,7 +325,8 @@ final albumId = albumId_example; // String |
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto | final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
try { try {
api_instance.removeAssetFromAlbum(albumId, removeAssetsDto); final result = api_instance.removeAssetFromAlbum(albumId, removeAssetsDto);
print(result);
} catch (e) { } catch (e) {
print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n'); print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n');
} }
@@ -340,7 +341,7 @@ Name | Type | Description | Notes
### Return type ### Return type
void (empty response body) [**AlbumResponseDto**](AlbumResponseDto.md)
### Authorization ### Authorization
@@ -349,7 +350,7 @@ void (empty response body)
### HTTP request headers ### HTTP request headers
- **Content-Type**: application/json - **Content-Type**: application/json
- **Accept**: Not defined - **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -311,7 +311,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAssetThumbnail** # **getAssetThumbnail**
> Object getAssetThumbnail(assetId) > Object getAssetThumbnail(assetId, format)
@@ -327,9 +327,10 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi(); final api_instance = AssetApi();
final assetId = assetId_example; // String | final assetId = assetId_example; // String |
final format = ; // ThumbnailFormat |
try { try {
final result = api_instance.getAssetThumbnail(assetId); final result = api_instance.getAssetThumbnail(assetId, format);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling AssetApi->getAssetThumbnail: $e\n'); print('Exception when calling AssetApi->getAssetThumbnail: $e\n');
@@ -341,6 +342,7 @@ try {
Name | Type | Description | Notes Name | Type | Description | Notes
------------- | ------------- | ------------- | ------------- ------------- | ------------- | ------------- | -------------
**assetId** | **String**| | **assetId** | **String**| |
**format** | [**ThumbnailFormat**](.md)| | [optional]
### Return type ### Return type

View File

@@ -11,6 +11,7 @@ Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**adminSignUp**](AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up | [**adminSignUp**](AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
[**login**](AuthenticationApi.md#login) | **POST** /auth/login | [**login**](AuthenticationApi.md#login) | **POST** /auth/login |
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | [**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
@@ -96,6 +97,43 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **logout**
> LogoutResponseDto logout()
### Example
```dart
import 'package:openapi/api.dart';
final api_instance = AuthenticationApi();
try {
final result = api_instance.logout();
print(result);
} catch (e) {
print('Exception when calling AuthenticationApi->logout: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**LogoutResponseDto**](LogoutResponseDto.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **validateAccessToken** # **validateAccessToken**
> ValidateAccessTokenResponseDto validateAccessToken() > ValidateAccessTokenResponseDto validateAccessToken()

View File

@@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**isExist** | **bool** | | **isExist** | **bool** | |
**id** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**albumName** | **String** | | **albumName** | **String** | | [optional]
**ownerId** | **String** | | **albumThumbnailAssetId** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -14,6 +14,7 @@ Method | HTTP request | Description
[**getAllUsers**](UserApi.md#getallusers) | **GET** /user | [**getAllUsers**](UserApi.md#getallusers) | **GET** /user |
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | [**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | [**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count | [**getUserCount**](UserApi.md#getusercount) | **GET** /user/count |
[**updateUser**](UserApi.md#updateuser) | **PUT** /user | [**updateUser**](UserApi.md#updateuser) | **PUT** /user |
@@ -243,6 +244,47 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getUserById**
> UserResponseDto getUserById(userId)
### Example
```dart
import 'package:openapi/api.dart';
final api_instance = UserApi();
final userId = userId_example; // String |
try {
final result = api_instance.getUserById(userId);
print(result);
} catch (e) {
print('Exception when calling UserApi->getUserById: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**userId** | **String**| |
### Return type
[**UserResponseDto**](UserResponseDto.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getUserCount** # **getUserCount**
> UserCountResponseDto getUserCount() > UserCountResponseDto getUserCount()

View File

@@ -57,6 +57,7 @@ part 'model/device_type_enum.dart';
part 'model/exif_response_dto.dart'; part 'model/exif_response_dto.dart';
part 'model/login_credential_dto.dart'; part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart'; part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
part 'model/remove_assets_dto.dart'; part 'model/remove_assets_dto.dart';
part 'model/search_asset_dto.dart'; part 'model/search_asset_dto.dart';
part 'model/server_info_response_dto.dart'; part 'model/server_info_response_dto.dart';
@@ -64,6 +65,7 @@ part 'model/server_ping_response.dart';
part 'model/server_version_reponse_dto.dart'; part 'model/server_version_reponse_dto.dart';
part 'model/sign_up_dto.dart'; part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart'; part 'model/smart_info_response_dto.dart';
part 'model/thumbnail_format.dart';
part 'model/update_album_dto.dart'; part 'model/update_album_dto.dart';
part 'model/update_device_info_dto.dart'; part 'model/update_device_info_dto.dart';
part 'model/update_user_dto.dart'; part 'model/update_user_dto.dart';

View File

@@ -346,11 +346,19 @@ class AlbumApi {
/// * [String] albumId (required): /// * [String] albumId (required):
/// ///
/// * [RemoveAssetsDto] removeAssetsDto (required): /// * [RemoveAssetsDto] removeAssetsDto (required):
Future<void> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto,) async { Future<AlbumResponseDto?> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto,) async {
final response = await removeAssetFromAlbumWithHttpInfo(albumId, removeAssetsDto,); final response = await removeAssetFromAlbumWithHttpInfo(albumId, removeAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumResponseDto',) as AlbumResponseDto;
}
return null;
} }
/// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response]. /// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response].

View File

@@ -346,7 +346,9 @@ class AssetApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] assetId (required): /// * [String] assetId (required):
Future<Response> getAssetThumbnailWithHttpInfo(String assetId,) async { ///
/// * [ThumbnailFormat] format:
Future<Response> getAssetThumbnailWithHttpInfo(String assetId, { ThumbnailFormat? format, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/asset/thumbnail/{assetId}' final path = r'/asset/thumbnail/{assetId}'
.replaceAll('{assetId}', assetId); .replaceAll('{assetId}', assetId);
@@ -358,6 +360,10 @@ class AssetApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (format != null) {
queryParams.addAll(_queryParams('', 'format', format));
}
const contentTypes = <String>[]; const contentTypes = <String>[];
@@ -375,8 +381,10 @@ class AssetApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] assetId (required): /// * [String] assetId (required):
Future<Object?> getAssetThumbnail(String assetId,) async { ///
final response = await getAssetThumbnailWithHttpInfo(assetId,); /// * [ThumbnailFormat] format:
Future<Object?> getAssetThumbnail(String assetId, { ThumbnailFormat? format, }) async {
final response = await getAssetThumbnailWithHttpInfo(assetId, format: format, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

@@ -110,6 +110,47 @@ class AuthenticationApi {
return null; return null;
} }
/// Performs an HTTP 'POST /auth/logout' operation and returns the [Response].
Future<Response> logoutWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/auth/logout';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<LogoutResponseDto?> logout() async {
final response = await logoutWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LogoutResponseDto',) as LogoutResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response]. /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
Future<Response> validateAccessTokenWithHttpInfo() async { Future<Response> validateAccessTokenWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations

View File

@@ -261,6 +261,54 @@ class UserApi {
return null; return null;
} }
/// Performs an HTTP 'GET /user/info/{userId}' operation and returns the [Response].
/// Parameters:
///
/// * [String] userId (required):
Future<Response> getUserByIdWithHttpInfo(String userId,) async {
// ignore: prefer_const_declarations
final path = r'/user/info/{userId}'
.replaceAll('{userId}', userId);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] userId (required):
Future<UserResponseDto?> getUserById(String userId,) async {
final response = await getUserByIdWithHttpInfo(userId,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /user/count' operation and returns the [Response]. /// Performs an HTTP 'GET /user/count' operation and returns the [Response].
Future<Response> getUserCountWithHttpInfo() async { Future<Response> getUserCountWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations

View File

@@ -238,6 +238,8 @@ class ApiClient {
return LoginCredentialDto.fromJson(value); return LoginCredentialDto.fromJson(value);
case 'LoginResponseDto': case 'LoginResponseDto':
return LoginResponseDto.fromJson(value); return LoginResponseDto.fromJson(value);
case 'LogoutResponseDto':
return LogoutResponseDto.fromJson(value);
case 'RemoveAssetsDto': case 'RemoveAssetsDto':
return RemoveAssetsDto.fromJson(value); return RemoveAssetsDto.fromJson(value);
case 'SearchAssetDto': case 'SearchAssetDto':
@@ -252,6 +254,8 @@ class ApiClient {
return SignUpDto.fromJson(value); return SignUpDto.fromJson(value);
case 'SmartInfoResponseDto': case 'SmartInfoResponseDto':
return SmartInfoResponseDto.fromJson(value); return SmartInfoResponseDto.fromJson(value);
case 'ThumbnailFormat':
return ThumbnailFormatTypeTransformer().decode(value);
case 'UpdateAlbumDto': case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value); return UpdateAlbumDto.fromJson(value);
case 'UpdateDeviceInfoDto': case 'UpdateDeviceInfoDto':

View File

@@ -64,6 +64,9 @@ String parameterToString(dynamic value) {
if (value is DeviceTypeEnum) { if (value is DeviceTypeEnum) {
return DeviceTypeEnumTypeTransformer().encode(value).toString(); return DeviceTypeEnumTypeTransformer().encode(value).toString();
} }
if (value is ThumbnailFormat) {
return ThumbnailFormatTypeTransformer().encode(value).toString();
}
return value.toString(); return value.toString();
} }

View File

@@ -14,25 +14,41 @@ class CheckDuplicateAssetResponseDto {
/// Returns a new [CheckDuplicateAssetResponseDto] instance. /// Returns a new [CheckDuplicateAssetResponseDto] instance.
CheckDuplicateAssetResponseDto({ CheckDuplicateAssetResponseDto({
required this.isExist, required this.isExist,
this.id,
}); });
bool isExist; bool isExist;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? id;
@override @override
bool operator ==(Object other) => identical(this, other) || other is CheckDuplicateAssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is CheckDuplicateAssetResponseDto &&
other.isExist == isExist; other.isExist == isExist &&
other.id == id;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(isExist.hashCode); (isExist.hashCode) +
(id == null ? 0 : id!.hashCode);
@override @override
String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist]'; String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist, id=$id]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'isExist'] = isExist; _json[r'isExist'] = isExist;
if (id != null) {
_json[r'id'] = id;
} else {
_json[r'id'] = null;
}
return _json; return _json;
} }
@@ -56,6 +72,7 @@ class CheckDuplicateAssetResponseDto {
return CheckDuplicateAssetResponseDto( return CheckDuplicateAssetResponseDto(
isExist: mapValueOfType<bool>(json, r'isExist')!, isExist: mapValueOfType<bool>(json, r'isExist')!,
id: mapValueOfType<String>(json, r'id'),
); );
} }
return null; return null;

View File

@@ -0,0 +1,111 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class LogoutResponseDto {
/// Returns a new [LogoutResponseDto] instance.
LogoutResponseDto({
required this.successful,
});
bool successful;
@override
bool operator ==(Object other) => identical(this, other) || other is LogoutResponseDto &&
other.successful == successful;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(successful.hashCode);
@override
String toString() => 'LogoutResponseDto[successful=$successful]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'successful'] = successful;
return _json;
}
/// Returns a new [LogoutResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static LogoutResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "LogoutResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "LogoutResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
return LogoutResponseDto(
successful: mapValueOfType<bool>(json, r'successful')!,
);
}
return null;
}
static List<LogoutResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <LogoutResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = LogoutResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, LogoutResponseDto> mapFromJson(dynamic json) {
final map = <String, LogoutResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = LogoutResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of LogoutResponseDto-objects as value to a dart map
static Map<String, List<LogoutResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<LogoutResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = LogoutResponseDto.listFromJson(entry.value, growable: growable,);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'successful',
};
}

View File

@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ThumbnailFormat {
/// Instantiate a new enum with the provided [value].
const ThumbnailFormat._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const JPEG = ThumbnailFormat._(r'JPEG');
static const WEBP = ThumbnailFormat._(r'WEBP');
/// List of all possible values in this [enum][ThumbnailFormat].
static const values = <ThumbnailFormat>[
JPEG,
WEBP,
];
static ThumbnailFormat? fromJson(dynamic value) => ThumbnailFormatTypeTransformer().decode(value);
static List<ThumbnailFormat>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <ThumbnailFormat>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ThumbnailFormat.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [ThumbnailFormat] to String,
/// and [decode] dynamic data back to [ThumbnailFormat].
class ThumbnailFormatTypeTransformer {
factory ThumbnailFormatTypeTransformer() => _instance ??= const ThumbnailFormatTypeTransformer._();
const ThumbnailFormatTypeTransformer._();
String encode(ThumbnailFormat data) => data.value;
/// Decodes a [dynamic value][data] to a ThumbnailFormat.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
ThumbnailFormat? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data.toString()) {
case r'JPEG': return ThumbnailFormat.JPEG;
case r'WEBP': return ThumbnailFormat.WEBP;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [ThumbnailFormatTypeTransformer] instance.
static ThumbnailFormatTypeTransformer? _instance;
}

View File

@@ -13,32 +13,52 @@ part of openapi.api;
class UpdateAlbumDto { class UpdateAlbumDto {
/// Returns a new [UpdateAlbumDto] instance. /// Returns a new [UpdateAlbumDto] instance.
UpdateAlbumDto({ UpdateAlbumDto({
required this.albumName, this.albumName,
required this.ownerId, this.albumThumbnailAssetId,
}); });
String albumName; ///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? albumName;
String ownerId; ///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? albumThumbnailAssetId;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
other.albumName == albumName && other.albumName == albumName &&
other.ownerId == ownerId; other.albumThumbnailAssetId == albumThumbnailAssetId;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumName.hashCode) + (albumName == null ? 0 : albumName!.hashCode) +
(ownerId.hashCode); (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode);
@override @override
String toString() => 'UpdateAlbumDto[albumName=$albumName, ownerId=$ownerId]'; String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
if (albumName != null) {
_json[r'albumName'] = albumName; _json[r'albumName'] = albumName;
_json[r'ownerId'] = ownerId; } else {
_json[r'albumName'] = null;
}
if (albumThumbnailAssetId != null) {
_json[r'albumThumbnailAssetId'] = albumThumbnailAssetId;
} else {
_json[r'albumThumbnailAssetId'] = null;
}
return _json; return _json;
} }
@@ -61,8 +81,8 @@ class UpdateAlbumDto {
}()); }());
return UpdateAlbumDto( return UpdateAlbumDto(
albumName: mapValueOfType<String>(json, r'albumName')!, albumName: mapValueOfType<String>(json, r'albumName'),
ownerId: mapValueOfType<String>(json, r'ownerId')!, albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
); );
} }
return null; return null;
@@ -112,8 +132,6 @@ class UpdateAlbumDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'albumName',
'ownerId',
}; };
} }

View File

@@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for LogoutResponseDto
void main() {
// final instance = LogoutResponseDto();
group('test LogoutResponseDto', () {
// bool successful
test('to test the property `successful`', () async {
// TODO
});
});
}

View File

@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ThumbnailFormat
void main() {
group('test ThumbnailFormat', () {
});
}

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.18.0+27 version: 1.19.0+29
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"

View File

@@ -15,5 +15,17 @@ def main():
print(f"Outdated Key! {k}") print(f"Outdated Key! {k}")
return 1 return 1
print("CHECK FRENCH TRANSLATIONS")
with open('assets/i18n/fr-FR.json', 'r') as f:
data = json.load(f)
for k in data.keys():
print(k)
sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
if sp.returncode != 0:
print(f"Outdated Key! {k}")
return 1
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -1,7 +1,7 @@
import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumEntity } from '@app/database/entities/album.entity';
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity'; import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
import { UserAlbumEntity } from '@app/database/entities/user-album.entity'; import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
import { Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, SelectQueryBuilder, DataSource } from 'typeorm'; import { Repository, SelectQueryBuilder, DataSource } from 'typeorm';
import { AddAssetsDto } from './dto/add-assets.dto'; import { AddAssetsDto } from './dto/add-assets.dto';
@@ -10,6 +10,7 @@ import { CreateAlbumDto } from './dto/create-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumResponseDto } from './response-dto/album-response.dto';
export interface IAlbumRepository { export interface IAlbumRepository {
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>; create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
@@ -18,7 +19,7 @@ export interface IAlbumRepository {
delete(album: AlbumEntity): Promise<void>; delete(album: AlbumEntity): Promise<void>;
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>; addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
removeUser(album: AlbumEntity, userId: string): Promise<void>; removeUser(album: AlbumEntity, userId: string): Promise<void>;
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<boolean>; removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>;
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>; addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>; updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
} }
@@ -84,7 +85,7 @@ export class AlbumRepository implements IAlbumRepository {
}); });
} }
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> { async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
const filteringByShared = typeof getAlbumsDto.shared == 'boolean'; const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
const userId = ownerId; const userId = ownerId;
let query = this.albumRepository.createQueryBuilder('album'); let query = this.albumRepository.createQueryBuilder('album');
@@ -132,35 +133,45 @@ export class AlbumRepository implements IAlbumRepository {
query = query query = query
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.where('album.ownerId = :ownerId', { ownerId: userId }) .where('album.ownerId = :ownerId', { ownerId: userId });
.orWhere((qb) => { // .orWhere((qb) => {
const subQuery = qb // const subQuery = qb
.subQuery() // .subQuery()
.select('userAlbum.albumId') // .select('userAlbum.albumId')
.from(UserAlbumEntity, 'userAlbum') // .from(UserAlbumEntity, 'userAlbum')
.where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId }) // .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
.getQuery(); // .getQuery();
return `album.id IN ${subQuery}`; // return `album.id IN ${subQuery}`;
}); // });
} }
return query.orderBy('album.createdAt', 'DESC').getMany(); // Get information of assets in albums
query = query
.leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
const albums = await query.getMany();
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
return albums;
} }
async get(albumId: string): Promise<AlbumEntity | undefined> { async get(albumId: string): Promise<AlbumEntity | undefined> {
const album = await this.albumRepository.findOne({ let query = this.albumRepository.createQueryBuilder('album');
where: { id: albumId },
relations: ['sharedUsers', 'sharedUsers.userInfo', 'assets', 'assets.assetInfo'], const album = await query
}); .where('album.id = :albumId', { albumId })
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.leftJoinAndSelect('assetInfo.exifInfo', 'exifInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
.getOne();
if (!album) { if (!album) {
return; return;
} }
// TODO: sort in query
const sortedSharedAsset = album.assets?.sort(
(a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(),
);
album.assets = sortedSharedAsset;
return album; return album;
} }
@@ -188,7 +199,7 @@ export class AlbumRepository implements IAlbumRepository {
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId }); await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
} }
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<boolean> { async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<AlbumEntity> {
let deleteAssetCount = 0; let deleteAssetCount = 0;
// TODO: should probably do a single delete query? // TODO: should probably do a single delete query?
for (const assetId of removeAssetsDto.assetIds) { for (const assetId of removeAssetsDto.assetIds) {
@@ -197,7 +208,11 @@ export class AlbumRepository implements IAlbumRepository {
} }
// TODO: No need to return boolean if using a singe delete query // TODO: No need to return boolean if using a singe delete query
return deleteAssetCount == removeAssetsDto.assetIds.length; if (deleteAssetCount == removeAssetsDto.assetIds.length) {
return this.get(album.id) as Promise<AlbumEntity>;
} else {
throw new BadRequestException('Some assets were not found in the album');
}
} }
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity> { async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity> {
@@ -222,7 +237,8 @@ export class AlbumRepository implements IAlbumRepository {
} }
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> { updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> {
album.albumName = updateAlbumDto.albumName; album.albumName = updateAlbumDto.albumName || album.albumName;
album.albumThumbnailAssetId = updateAlbumDto.albumThumbnailAssetId || album.albumThumbnailAssetId;
return this.albumRepository.save(album); return this.albumRepository.save(album);
} }

View File

@@ -23,6 +23,7 @@ import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from './response-dto/album-response.dto';
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe. // TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@@ -76,7 +77,7 @@ export class AlbumController {
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto, @Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
) { ): Promise<AlbumResponseDto> {
return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId); return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
} }
@@ -103,6 +104,6 @@ export class AlbumController {
@Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto, @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
) { ) {
return this.albumService.updateAlbumTitle(authUser, updateAlbumInfoDto, albumId); return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
} }
} }

View File

@@ -260,17 +260,16 @@ describe('Album service', () => {
const albumEntity = _getOwnedAlbum(); const albumEntity = _getOwnedAlbum();
const albumId = albumEntity.id; const albumId = albumEntity.id;
const updatedAlbumName = 'new album name'; const updatedAlbumName = 'new album name';
const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.updateAlbum.mockImplementation(() => albumRepositoryMock.updateAlbum.mockImplementation(() =>
Promise.resolve<AlbumEntity>({ ...albumEntity, albumName: updatedAlbumName }), Promise.resolve<AlbumEntity>({ ...albumEntity, albumName: updatedAlbumName }),
); );
const result = await sut.updateAlbumTitle( const result = await sut.updateAlbumInfo(
authUser, authUser,
{ {
albumName: updatedAlbumName, albumName: updatedAlbumName,
ownerId: 'this is not used and will be removed',
}, },
albumId, albumId,
); );
@@ -280,7 +279,7 @@ describe('Album service', () => {
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1); expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, { expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumName: updatedAlbumName, albumName: updatedAlbumName,
ownerId: 'this is not used and will be removed', thumbnailAssetId: updatedAlbumThumbnailAssetId,
}); });
}); });
@@ -291,11 +290,11 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( await expect(
sut.updateAlbumTitle( sut.updateAlbumInfo(
authUser, authUser,
{ {
albumName: 'new album name', albumName: 'new album name',
ownerId: 'this is not used and will be removed', albumThumbnailAssetId: '69d2f917-0b31-48d8-9d7d-673b523f1aac',
}, },
albumId, albumId,
), ),
@@ -361,7 +360,7 @@ describe('Album service', () => {
it('removes assets from owned album', async () => { it('removes assets from owned album', async () => {
const albumEntity = _getOwnedAlbum(); const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve(true)); albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( await expect(
sut.removeAssetsFromAlbum( sut.removeAssetsFromAlbum(
@@ -381,7 +380,7 @@ describe('Album service', () => {
it('removes assets from shared album (shared with auth user)', async () => { it('removes assets from shared album (shared with auth user)', async () => {
const albumEntity = _getOwnedSharedAlbum(); const albumEntity = _getOwnedSharedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve(true)); albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( await expect(
sut.removeAssetsFromAlbum( sut.removeAssetsFromAlbum(

View File

@@ -82,9 +82,15 @@ export class AlbumService {
// async removeUsersFromAlbum() {} // async removeUsersFromAlbum() {}
async removeAssetsFromAlbum(authUser: AuthUserDto, removeAssetsDto: RemoveAssetsDto, albumId: string): Promise<void> { async removeAssetsFromAlbum(
authUser: AuthUserDto,
removeAssetsDto: RemoveAssetsDto,
albumId: string,
): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
await this._albumRepository.removeAssets(album, removeAssetsDto); const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
return mapAlbum(updateAlbum);
} }
async addAssetsToAlbum( async addAssetsToAlbum(
@@ -97,16 +103,17 @@ export class AlbumService {
return mapAlbum(updatedAlbum); return mapAlbum(updatedAlbum);
} }
async updateAlbumTitle( async updateAlbumInfo(
authUser: AuthUserDto, authUser: AuthUserDto,
updateAlbumDto: UpdateAlbumDto, updateAlbumDto: UpdateAlbumDto,
albumId: string, albumId: string,
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
// TODO: this should not come from request DTO. To be removed from here and DTO
// if (authUser.id != updateAlbumDto.ownerId) {
// throw new BadRequestException('Unauthorized to change album info');
// }
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
if (authUser.id != album.ownerId) {
throw new BadRequestException('Unauthorized to change album info');
}
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto); const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
return mapAlbum(updatedAlbum); return mapAlbum(updatedAlbum);
} }

View File

@@ -1,9 +1,9 @@
import { IsNotEmpty } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@IsNotEmpty() @IsOptional()
albumName!: string; albumName?: string;
@IsNotEmpty() @IsOptional()
ownerId!: string; albumThumbnailAssetId?: string;
} }

View File

@@ -43,6 +43,7 @@ import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@@ -109,8 +110,11 @@ export class AssetController {
} }
@Get('/thumbnail/:assetId') @Get('/thumbnail/:assetId')
async getAssetThumbnail(@Param('assetId') assetId: string): Promise<any> { async getAssetThumbnail(
return this.assetService.getAssetThumbnail(assetId); @Param('assetId') assetId: string,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
): Promise<any> {
return this.assetService.getAssetThumbnail(assetId, query);
} }
@Get('/allObjects') @Get('/allObjects')
@@ -198,8 +202,6 @@ export class AssetController {
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto, @Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto,
): Promise<CheckDuplicateAssetResponseDto> { ): Promise<CheckDuplicateAssetResponseDto> {
const res = await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto); return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
return new CheckDuplicateAssetResponseDto(res);
} }
} }

View File

@@ -23,6 +23,8 @@ import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
import { AssetFileUploadDto } from './dto/asset-file-upload.dto'; import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@@ -187,7 +189,7 @@ export class AssetService {
} }
} }
public async getAssetThumbnail(assetId: string) { public async getAssetThumbnail(assetId: string, query: GetAssetThumbnailDto) {
let fileReadStream: ReadStream; let fileReadStream: ReadStream;
const asset = await this.assetRepository.findOne({ where: { id: assetId } }); const asset = await this.assetRepository.findOne({ where: { id: assetId } });
@@ -197,16 +199,25 @@ export class AssetService {
} }
try { try {
if (asset.webpPath && asset.webpPath.length > 0) { if (query.format == GetAssetThumbnailFormatEnum.JPEG) {
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.webpPath);
} else {
if (!asset.resizePath) { if (!asset.resizePath) {
throw new NotFoundException('resizePath not set'); throw new NotFoundException('resizePath not set');
} }
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK); await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.resizePath); fileReadStream = createReadStream(asset.resizePath);
} else {
if (asset.webpPath && asset.webpPath.length > 0) {
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.webpPath);
} else {
if (!asset.resizePath) {
throw new NotFoundException('resizePath not set');
}
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.resizePath);
}
} }
return new StreamableFile(fileReadStream); return new StreamableFile(fileReadStream);
@@ -477,7 +488,10 @@ export class AssetService {
return curatedObjects; return curatedObjects;
} }
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto): Promise<boolean> { async checkDuplicatedAsset(
authUser: AuthUserDto,
checkDuplicateAssetDto: CheckDuplicateAssetDto,
): Promise<CheckDuplicateAssetResponseDto> {
const res = await this.assetRepository.findOne({ const res = await this.assetRepository.findOne({
where: { where: {
deviceAssetId: checkDuplicateAssetDto.deviceAssetId, deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
@@ -486,6 +500,8 @@ export class AssetService {
}, },
}); });
return res ? true : false; const isDuplicated = res ? true : false;
return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
} }
} }

View File

@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export enum GetAssetThumbnailFormatEnum {
JPEG = 'JPEG',
WEBP = 'WEBP',
}
export class GetAssetThumbnailDto {
@IsOptional()
@ApiProperty({
enum: GetAssetThumbnailFormatEnum,
default: GetAssetThumbnailFormatEnum.WEBP,
required: false,
enumName: 'ThumbnailFormat',
})
format = GetAssetThumbnailFormatEnum.WEBP;
}

View File

@@ -1,6 +1,8 @@
export class CheckDuplicateAssetResponseDto { export class CheckDuplicateAssetResponseDto {
constructor(isExist: boolean) { constructor(isExist: boolean, id?: string) {
this.isExist = isExist; this.isExist = isExist;
this.id = id;
} }
isExist: boolean; isExist: boolean;
id?: string;
} }

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Post, UseGuards, ValidationPipe } from '@nestjs/common'; import { Body, Controller, Post, Res, UseGuards, ValidationPipe } from '@nestjs/common';
import { import {
ApiBadRequestResponse, ApiBadRequestResponse,
ApiBearerAuth, ApiBearerAuth,
@@ -15,6 +15,8 @@ import { LoginResponseDto } from './response-dto/login-response.dto';
import { SignUpDto } from './dto/sign-up.dto'; import { SignUpDto } from './dto/sign-up.dto';
import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto'; import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,'; import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,';
import { Response } from 'express';
import { LogoutResponseDto } from './response-dto/logout-response.dto';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
@@ -22,8 +24,20 @@ export class AuthController {
constructor(private readonly authService: AuthService) {} constructor(private readonly authService: AuthService) {}
@Post('/login') @Post('/login')
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto): Promise<LoginResponseDto> { async login(
return await this.authService.login(loginCredential); @Body(ValidationPipe) loginCredential: LoginCredentialDto,
@Res() response: Response,
): Promise<LoginResponseDto> {
const loginResponse = await this.authService.login(loginCredential);
// Set Cookies
const accessTokenCookie = this.authService.getCookieWithJwtToken(loginResponse);
const isAuthCookie = `immich_is_authenticated=true; Path=/; Max-Age=${7 * 24 * 3600}`;
response.setHeader('Set-Cookie', [accessTokenCookie, isAuthCookie]);
response.send(loginResponse);
return loginResponse;
} }
@Post('/admin-sign-up') @Post('/admin-sign-up')
@@ -39,4 +53,16 @@ export class AuthController {
async validateAccessToken(@GetAuthUser() authUser: AuthUserDto): Promise<ValidateAccessTokenResponseDto> { async validateAccessToken(@GetAuthUser() authUser: AuthUserDto): Promise<ValidateAccessTokenResponseDto> {
return new ValidateAccessTokenResponseDto(true); return new ValidateAccessTokenResponseDto(true);
} }
@Post('/logout')
async logout(@Res() response: Response): Promise<LogoutResponseDto> {
response.clearCookie('immich_access_token');
response.clearCookie('immich_is_authenticated');
const status = new LogoutResponseDto(true);
response.send(status)
return status;
}
} }

View File

@@ -63,6 +63,12 @@ export class AuthService {
return mapLoginResponse(validatedUser, accessToken); return mapLoginResponse(validatedUser, accessToken);
} }
public getCookieWithJwtToken(authLoginInfo: LoginResponseDto) {
const maxAge = 7 * 24 * 3600; // 7 days
return `immich_access_token=${authLoginInfo.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}`;
}
// !TODO: refactor this method to use the userService createUser method
public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> { public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } }); const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });

View File

@@ -0,0 +1,27 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { SignUpDto } from './sign-up.dto';
describe('sign up DTO', () => {
it('validates the email', async () => {
const params: Partial<SignUpDto> = {
email: undefined,
password: 'password',
firstName: 'first name',
lastName: 'last name',
};
let dto: SignUpDto = plainToInstance(SignUpDto, params);
let errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'invalid email';
dto = plainToInstance(SignUpDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'valid@email.com';
dto = plainToInstance(SignUpDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(0);
});
});

View File

@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class SignUpDto { export class SignUpDto {
@IsNotEmpty() @IsEmail()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
email!: string; email!: string;

View File

@@ -0,0 +1,10 @@
import { ApiResponseProperty } from '@nestjs/swagger';
export class LogoutResponseDto {
constructor (successful: boolean) {
this.successful = successful;
}
@ApiResponseProperty()
successful!: boolean;
};

View File

@@ -7,11 +7,12 @@ import { ApiTags } from '@nestjs/swagger';
import { ServerPingResponse } from './response-dto/server-ping-response.dto'; import { ServerPingResponse } from './response-dto/server-ping-response.dto';
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto'; import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto'; import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
import { DataSource } from 'typeorm';
@ApiTags('Server Info') @ApiTags('Server Info')
@Controller('server-info') @Controller('server-info')
export class ServerInfoController { export class ServerInfoController {
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {} constructor(private readonly serverInfoService: ServerInfoService) {}
@Get() @Get()
async getServerInfo(): Promise<ServerInfoResponseDto> { async getServerInfo(): Promise<ServerInfoResponseDto> {

View File

@@ -0,0 +1,27 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { CreateUserDto } from './create-user.dto';
describe('create user DTO', () => {
it('validates the email', async() => {
const params: Partial<CreateUserDto> = {
email: undefined,
password: 'password',
firstName: 'first name',
lastName: 'last name',
}
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
let errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'invalid email';
dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(1);
params.email = 'valid@email.com';
dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto);
expect(errors).toHaveLength(0);
});
});

View File

@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class CreateUserDto { export class CreateUserDto {
@IsNotEmpty() @IsEmail()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
email!: string; email!: string;

View File

@@ -0,0 +1,95 @@
import { UserEntity } from '@app/database/entities/user.entity';
import { BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcrypt';
import { UpdateUserDto } from './dto/update-user.dto'
export interface IUserRepository {
get(userId: string): Promise<UserEntity | null>;
getByEmail(email: string): Promise<UserEntity | null>;
getList(filter?: { excludeId?: string }): Promise<UserEntity[]>;
create(createUserDto: CreateUserDto): Promise<UserEntity>;
update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity>;
createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity>;
}
export const USER_REPOSITORY = 'USER_REPOSITORY';
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
private async hashPassword(password: string, salt: string): Promise<string> {
return bcrypt.hash(password, salt);
}
async get(userId: string): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { id: userId } });
}
async getByEmail(email: string): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { email } });
}
// TODO add DTO for filtering
async getList({ excludeId }: { excludeId?: string } = {}): Promise<UserEntity[]> {
if (!excludeId) {
return this.userRepository.find(); // TODO: this should also be ordered the same as below
}
return this.userRepository.find({
where: { id: Not(excludeId) },
order: {
createdAt: 'DESC',
},
});
}
async create(createUserDto: CreateUserDto): Promise<UserEntity> {
const newUser = new UserEntity();
newUser.email = createUserDto.email;
newUser.salt = await bcrypt.genSalt();
newUser.password = await this.hashPassword(createUserDto.password, newUser.salt);
newUser.firstName = createUserDto.firstName;
newUser.lastName = createUserDto.lastName;
newUser.isAdmin = false;
return this.userRepository.save(newUser);
}
async update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity> {
user.lastName = updateUserDto.lastName || user.lastName;
user.firstName = updateUserDto.firstName || user.firstName;
user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath;
user.shouldChangePassword =
updateUserDto.shouldChangePassword != undefined ? updateUserDto.shouldChangePassword : user.shouldChangePassword;
// If payload includes password - Create new password for user
if (updateUserDto.password) {
user.salt = await bcrypt.genSalt();
user.password = await this.hashPassword(updateUserDto.password, user.salt);
}
// TODO: can this happen? If so we can move it to the service, otherwise remove it (also from DTO)
if (updateUserDto.isAdmin) {
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
if (adminUser) {
throw new BadRequestException('Admin user exists');
}
user.isAdmin = true;
}
return this.userRepository.save(user);
}
async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity> {
user.profileImagePath = fileInfo.path;
return this.userRepository.save(user);
}
}

View File

@@ -45,6 +45,11 @@ export class UserController {
return await this.userService.getAllUsers(authUser, isAll); return await this.userService.getAllUsers(authUser, isAll);
} }
@Get('/info/:userId')
async getUserById(@Param('userId') userId: string): Promise<UserResponseDto> {
return await this.userService.getUserById(userId);
}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@Get('me') @Get('me')

View File

@@ -7,10 +7,18 @@ import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../../config/jwt.config'; import { jwtConfig } from '../../config/jwt.config';
import { UserRepository, USER_REPOSITORY } from './user-repository';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)], imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
controllers: [UserController], controllers: [UserController],
providers: [UserService, ImmichJwtService], providers: [
UserService,
ImmichJwtService,
{
provide: USER_REPOSITORY,
useClass: UserRepository
}
],
}) })
export class UserModule {} export class UserModule {}

View File

@@ -1,18 +1,15 @@
import { import {
BadRequestException, BadRequestException,
Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
NotFoundException, NotFoundException,
StreamableFile, StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateUserDto } from './dto/create-user.dto'; import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from '@app/database/entities/user.entity';
import * as bcrypt from 'bcrypt';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
import { mapUser, UserResponseDto } from './response-dto/user-response.dto'; import { mapUser, UserResponseDto } from './response-dto/user-response.dto';
@@ -21,32 +18,37 @@ import {
CreateProfileImageResponseDto, CreateProfileImageResponseDto,
mapCreateProfileImageResponse, mapCreateProfileImageResponse,
} from './response-dto/create-profile-image-response.dto'; } from './response-dto/create-profile-image-response.dto';
import { IUserRepository, USER_REPOSITORY } from './user-repository';
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor( constructor(
@InjectRepository(UserEntity) @Inject(USER_REPOSITORY)
private userRepository: Repository<UserEntity>, private userRepository: IUserRepository,
) {} ) {}
async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> { async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
if (isAll) { if (isAll) {
const allUsers = await this.userRepository.find(); const allUsers = await this.userRepository.getList();
return allUsers.map(mapUser); return allUsers.map(mapUser);
} }
const allUserExceptRequestedUser = await this.userRepository.find({ const allUserExceptRequestedUser = await this.userRepository.getList({ excludeId: authUser.id });
where: { id: Not(authUser.id) },
order: {
createdAt: 'DESC',
},
});
return allUserExceptRequestedUser.map(mapUser); return allUserExceptRequestedUser.map(mapUser);
} }
async getUserById(userId: string): Promise<UserResponseDto> {
const user = await this.userRepository.get(userId);
if (!user) {
throw new NotFoundException('User not found');
}
return mapUser(user);
}
async getUserInfo(authUser: AuthUserDto): Promise<UserResponseDto> { async getUserInfo(authUser: AuthUserDto): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id: authUser.id } }); const user = await this.userRepository.get(authUser.id);
if (!user) { if (!user) {
throw new BadRequestException('User not found'); throw new BadRequestException('User not found');
} }
@@ -54,28 +56,20 @@ export class UserService {
} }
async getUserCount(): Promise<UserCountResponseDto> { async getUserCount(): Promise<UserCountResponseDto> {
const users = await this.userRepository.find(); const users = await this.userRepository.getList();
return mapUserCountResponse(users.length); return mapUserCountResponse(users.length);
} }
async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> { async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { email: createUserDto.email } }); const user = await this.userRepository.getByEmail(createUserDto.email);
if (user) { if (user) {
throw new BadRequestException('User exists'); throw new BadRequestException('User exists');
} }
const newUser = new UserEntity();
newUser.email = createUserDto.email;
newUser.salt = await bcrypt.genSalt();
newUser.password = await this.hashPassword(createUserDto.password, newUser.salt);
newUser.firstName = createUserDto.firstName;
newUser.lastName = createUserDto.lastName;
newUser.isAdmin = false;
try { try {
const savedUser = await this.userRepository.save(newUser); const savedUser = await this.userRepository.create(createUserDto);
return mapUser(savedUser); return mapUser(savedUser);
} catch (e) { } catch (e) {
@@ -84,40 +78,13 @@ export class UserService {
} }
} }
private async hashPassword(password: string, salt: string): Promise<string> {
return bcrypt.hash(password, salt);
}
async updateUser(updateUserDto: UpdateUserDto): Promise<UserResponseDto> { async updateUser(updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id: updateUserDto.id } }); const user = await this.userRepository.get(updateUserDto.id);
if (!user) { if (!user) {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
} }
user.lastName = updateUserDto.lastName || user.lastName;
user.firstName = updateUserDto.firstName || user.firstName;
user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath;
user.shouldChangePassword =
updateUserDto.shouldChangePassword != undefined ? updateUserDto.shouldChangePassword : user.shouldChangePassword;
// If payload includes password - Create new password for user
if (updateUserDto.password) {
user.salt = await bcrypt.genSalt();
user.password = await this.hashPassword(updateUserDto.password, user.salt);
}
if (updateUserDto.isAdmin) {
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
if (adminUser) {
throw new BadRequestException('Admin user exists');
}
user.isAdmin = true;
}
try { try {
const updatedUser = await this.userRepository.save(user); const updatedUser = await this.userRepository.update(user, updateUserDto);
return mapUser(updatedUser); return mapUser(updatedUser);
} catch (e) { } catch (e) {
@@ -130,10 +97,13 @@ export class UserService {
authUser: AuthUserDto, authUser: AuthUserDto,
fileInfo: Express.Multer.File, fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> { ): Promise<CreateProfileImageResponseDto> {
const user = await this.userRepository.get(authUser.id);
if (!user) {
throw new NotFoundException('User not found');
}
try { try {
await this.userRepository.update(authUser.id, { await this.userRepository.createProfileImage(user, fileInfo);
profileImagePath: fileInfo.path,
});
return mapCreateProfileImageResponse(authUser.id, fileInfo.path); return mapCreateProfileImageResponse(authUser.id, fileInfo.path);
} catch (e) { } catch (e) {
@@ -144,7 +114,7 @@ export class UserService {
async getUserProfileImage(userId: string, res: Res) { async getUserProfileImage(userId: string, res: Res) {
try { try {
const user = await this.userRepository.findOne({ where: { id: userId } }); const user = await this.userRepository.get(userId);
if (!user) { if (!user) {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
} }

View File

@@ -3,5 +3,5 @@ import { jwtSecret } from '../constants/jwt.constant';
export const jwtConfig: JwtModuleOptions = { export const jwtConfig: JwtModuleOptions = {
secret: jwtSecret, secret: jwtSecret,
signOptions: { expiresIn: '36500d' }, signOptions: { expiresIn: '7d' },
}; };

View File

@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = { export const serverVersion: IServerVersion = {
major: 1, major: 1,
minor: 18, minor: 19,
patch: 0, patch: 0,
build: 26, build: 0,
}; };

View File

@@ -1,7 +1,9 @@
import { dataSource } from '@app/database/config/database.config';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
import cookieParser from 'cookie-parser';
import { writeFileSync } from 'fs'; import { writeFileSync } from 'fs';
import path from 'path'; import path from 'path';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
@@ -11,7 +13,7 @@ async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy'); app.set('trust proxy');
app.use(cookieParser());
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
app.enableCors(); app.enableCors();
} }
@@ -24,7 +26,7 @@ async function bootstrap() {
.setVersion('1.17.0') .setVersion('1.17.0')
.addBearerAuth({ .addBearerAuth({
type: 'http', type: 'http',
scheme: 'bearer', scheme: 'Bearer',
bearerFormat: 'JWT', bearerFormat: 'JWT',
name: 'JWT', name: 'JWT',
description: 'Enter JWT token', description: 'Enter JWT token',
@@ -46,7 +48,6 @@ async function bootstrap() {
customSiteTitle: 'Immich API Documentation', customSiteTitle: 'Immich API Documentation',
}); });
await app.listen(3001, () => { await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') { if (process.env.NODE_ENV == 'development') {
// Generate API Documentation only in development mode // Generate API Documentation only in development mode

View File

@@ -16,23 +16,27 @@ export class AdminRolesGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
let accessToken = '';
if (request.headers['authorization']) { if (request.headers['authorization']) {
const bearerToken = request.headers['authorization'].split(' ')[1]; accessToken = request.headers['authorization'].split(' ')[1];
const { userId } = await this.jwtService.validateToken(bearerToken); } else if (request.cookies['immich_access_token']) {
accessToken = request.cookies['immich_access_token'];
if (!userId) { } else {
return false; return false;
}
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
return false;
}
return user.isAdmin;
} }
return false; const { userId } = await this.jwtService.validateToken(accessToken);
if (!userId) {
return false;
}
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
return false;
}
return user.isAdmin;
} }
} }

View File

@@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto'; import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
import { jwtSecret } from '../../constants/jwt.constant'; import { jwtSecret } from '../../constants/jwt.constant';
@@ -33,4 +34,24 @@ export class ImmichJwtService {
}; };
} }
} }
public extractJwtFromHeader(req: Request) {
if (
req.headers.authorization &&
(req.headers.authorization.split(' ')[0] === 'Bearer' || req.headers.authorization.split(' ')[0] === 'bearer')
) {
const accessToken = req.headers.authorization.split(' ')[1];
return accessToken;
}
return null;
}
public extractJwtFromCookie(req: Request) {
if (req.cookies?.immich_access_token) {
return req.cookies.immich_access_token;
}
return null;
}
} }

View File

@@ -6,15 +6,21 @@ import { Repository } from 'typeorm';
import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto'; import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
import { UserEntity } from '@app/database/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { jwtSecret } from '../../../constants/jwt.constant'; import { jwtSecret } from '../../../constants/jwt.constant';
import { ImmichJwtService } from '../immich-jwt.service';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor( constructor(
@InjectRepository(UserEntity) @InjectRepository(UserEntity)
private usersRepository: Repository<UserEntity>, private usersRepository: Repository<UserEntity>,
private immichJwtService: ImmichJwtService,
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromExtractors([
immichJwtService.extractJwtFromCookie,
immichJwtService.extractJwtFromHeader,
]),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: jwtSecret, secretOrKey: jwtSecret,
}); });

View File

@@ -10,6 +10,7 @@ import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
import { AuthUserDto } from '../src/decorators/auth-user.decorator'; import { AuthUserDto } from '../src/decorators/auth-user.decorator';
import { UserService } from '../src/api-v1/user/user.service'; import { UserService } from '../src/api-v1/user/user.service';
import { UserModule } from '../src/api-v1/user/user.module'; import { UserModule } from '../src/api-v1/user/user.module';
import { DataSource } from 'typeorm';
function _createAlbum(app: INestApplication, data: CreateAlbumDto) { function _createAlbum(app: INestApplication, data: CreateAlbumDto) {
return request(app.getHttpServer()).post('/album').send(data); return request(app.getHttpServer()).post('/album').send(data);
@@ -17,9 +18,10 @@ function _createAlbum(app: INestApplication, data: CreateAlbumDto) {
describe('Album', () => { describe('Album', () => {
let app: INestApplication; let app: INestApplication;
let database: DataSource;
afterAll(async () => { afterAll(async () => {
await clearDb(); await clearDb(database);
await app.close(); await app.close();
}); });
@@ -30,6 +32,7 @@ describe('Album', () => {
}).compile(); }).compile();
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
database = app.get(DataSource);
await app.init(); await app.init();
}); });
@@ -56,12 +59,14 @@ describe('Album', () => {
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
userService = app.get(UserService); userService = app.get(UserService);
database = app.get(DataSource);
await app.init(); await app.init();
}); });
describe('with empty DB', () => { describe('with empty DB', () => {
afterEach(async () => { afterEach(async () => {
await clearDb(); await clearDb(database);
}); });
it('creates an album', async () => { it('creates an album', async () => {
@@ -124,12 +129,11 @@ describe('Album', () => {
it('returns the album collection including owned and shared', async () => { it('returns the album collection including owned and shared', async () => {
const { status, body } = await request(app.getHttpServer()).get('/album'); const { status, body } = await request(app.getHttpServer()).get('/album');
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(3); expect(body).toHaveLength(2);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ ownerId: userOne.id, albumName: userOneShared, shared: true }), expect.objectContaining({ ownerId: userOne.id, albumName: userOneShared, shared: true }),
expect.objectContaining({ ownerId: userOne.id, albumName: userOneNotShared, shared: false }), expect.objectContaining({ ownerId: userOne.id, albumName: userOneNotShared, shared: false }),
expect.objectContaining({ ownerId: userTwo.id, albumName: userTwoShared, shared: true }),
]), ]),
); );
}); });

View File

@@ -3,13 +3,10 @@ import { CanActivate, ExecutionContext } from '@nestjs/common';
import { TestingModuleBuilder } from '@nestjs/testing'; import { TestingModuleBuilder } from '@nestjs/testing';
import { AuthUserDto } from '../src/decorators/auth-user.decorator'; import { AuthUserDto } from '../src/decorators/auth-user.decorator';
import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard';
import { databaseConfig } from '@app/database/config/database.config';
type CustomAuthCallback = () => AuthUserDto; type CustomAuthCallback = () => AuthUserDto;
export async function clearDb() { export async function clearDb(db: DataSource) {
const db = new DataSource(databaseConfig);
const entities = db.entityMetadatas; const entities = db.entityMetadatas;
for (const entity of entities) { for (const entity of entities) {
const repository = db.getRepository(entity.name); const repository = db.getRepository(entity.name);

View File

@@ -9,6 +9,7 @@ import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
import { UserService } from '../src/api-v1/user/user.service'; import { UserService } from '../src/api-v1/user/user.service';
import { CreateUserDto } from '../src/api-v1/user/dto/create-user.dto'; import { CreateUserDto } from '../src/api-v1/user/dto/create-user.dto';
import { UserResponseDto } from '../src/api-v1/user/response-dto/user-response.dto'; import { UserResponseDto } from '../src/api-v1/user/response-dto/user-response.dto';
import { DataSource } from 'typeorm';
function _createUser(userService: UserService, data: CreateUserDto) { function _createUser(userService: UserService, data: CreateUserDto) {
return userService.createUser(data); return userService.createUser(data);
@@ -16,9 +17,10 @@ function _createUser(userService: UserService, data: CreateUserDto) {
describe('User', () => { describe('User', () => {
let app: INestApplication; let app: INestApplication;
let database: DataSource;
afterAll(async () => { afterAll(async () => {
await clearDb(); await clearDb(database);
await app.close(); await app.close();
}); });
@@ -29,6 +31,7 @@ describe('User', () => {
}).compile(); }).compile();
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
database = app.get(DataSource);
await app.init(); await app.init();
}); });
@@ -54,6 +57,7 @@ describe('User', () => {
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication();
userService = app.get(UserService); userService = app.get(UserService);
database = app.get(DataSource);
await app.init(); await app.init();
}); });

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
import {DataSource} from "typeorm"; import { DataSource } from 'typeorm';
export const databaseConfig: PostgresConnectionOptions = { export const databaseConfig: PostgresConnectionOptions = {
type: 'postgres', type: 'postgres',

View File

@@ -74,7 +74,7 @@ export class ExifEntity {
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset?: ExifEntity; asset?: ExifEntity;
@Index("exif_text_searchable", { synchronize: false }) @Index('exif_text_searchable', { synchronize: false })
@Column({ @Column({
type: 'tsvector', type: 'tsvector',
generatedType: 'STORED', generatedType: 'STORED',
@@ -83,9 +83,10 @@ export class ExifEntity {
COALESCE(model, '') || ' ' || COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' || COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' || COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' || COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' || COALESCE("state", '') || ' ' ||
COALESCE("country", ''))` COALESCE("country", ''))`,
}) })
exifTextSearchableColumn!: string exifTextSearchableColumn!: string;
} }

View File

@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddExifImageNameAsSearchableText1658860470248 implements MigrationInterface {
name = 'AddExifImageNameAsSearchableText1658860470248';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))) STORED`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
[
'immich',
'public',
'exif',
'GENERATED_COLUMN',
'exifTextSearchableColumn',
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"imageName\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
],
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
['immich', 'public', 'exif', 'GENERATED_COLUMN', 'exifTextSearchableColumn', ''],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector NOT NULL`);
}
}

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