Compare commits

...

20 Commits

Author SHA1 Message Date
Alex The Bot
f32c02bd25 Version v1.106.0 2024-06-10 17:50:00 +00:00
Zack Pollard
b16c9405d8 docs: otel metrics port worker split (#10085) 2024-06-10 12:44:10 -05:00
Alex
46df165ef2 feat(mobile): compatibility message warning (#10065)
* feat(mobile): compatibility message warning

* refactor and better signature
2024-06-10 12:43:54 -05:00
Zack Pollard
19e35d8d3f chore(server): remove unused imagemin type dependency (#10084) 2024-06-10 17:08:25 +00:00
Alex
c4c070569f fix(web): mouse-wheel scrolling on detail panel is disabled (#10080) 2024-06-10 12:05:52 -05:00
Jason Rasmussen
7651f70c88 fix(server): asset delete logic (#10077)
* fix(server): asset delete logic

* test: e2e
2024-06-10 13:04:34 -04:00
Alex
4698c39855 chore: remove pr labeler requirement (#10081) 2024-06-10 12:59:19 -04:00
Zack Pollard
2f2aecfb47 fix(server): otel not working due to port conflicts after combining containers (#10078)
fix: otel not working due to port conflicts after combining containers

Fixes #9759
2024-06-10 16:01:04 +00:00
dependabot[bot]
20efd82461 chore(deps): bump docker/build-push-action from 5.3.0 to 5.4.0 (#10069)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.3.0...v5.4.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>
2024-06-10 14:52:50 +01:00
Zack Pollard
22a0b4d900 chore(web): order json files alphabetically (#10076) 2024-06-10 09:37:21 -04:00
Weblate (bot)
2f25a8a437 chore: update translations (#10075)
chore:  (Vietnamese)

Currently translated at 0.3% (3 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:16:21 +01:00
Weblate (bot)
7a0bc0ea87 chore: update translations (#10074)
chore:  (Vietnamese)

Currently translated at 0.2% (2 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:09:12 +01:00
Weblate (bot)
a564c80193 chore: update translations (#10073)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:06:20 +01:00
Weblate (bot)
f4671617d1 chore: update translations (#10072)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:03:48 +01:00
Zack Pollard
d331da0ced chore(web): fix weblate conflicts (#10071)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Spanish)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (Arabic)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/

* chore:  (Catalan)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/

* chore:  (Danish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/

* chore:  (Finnish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/

* chore:  (French)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Hebrew)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/

* chore:  (Hindi)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Italian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Japanese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/

* chore:  (Korean)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/

* chore:  (Lithuanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/

* chore:  (Latvian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/

* chore:  (Mongolian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mn/

* chore:  (Norwegian Bokmål)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/

* chore:  (Polish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Portuguese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/

* chore:  (Romanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/

* chore:  (Russian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Slovak)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/

* chore:  (Slovenian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/

* chore:  (Serbian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/

* chore:  (Swedish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/

* chore:  (Thai)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/

* chore:  (Ukrainian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/

* chore:  (Vietnamese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

* chore:  (Czech)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore(web): enable prettier for json files in web

---------

Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: LLL <326867814@qq.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
2024-06-10 13:59:54 +01:00
aviv926
84da9abcbc docs: Add Email Notifications info (#9930)
* Add Email Notifications info

* remove spaces

* Add ` ` to smtp link

* Small fixes

* PR feedback

* npm run format:fix

* PR feedback

* update docs

* formatting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-09 20:07:08 +00:00
Robert Schäfer
48eede59b9 refactor: dedicated icon for permanently delete (#10052)
Motivation
----------
It's a follow up to #10028. I think it would be better user experience if one can tell by the icon what the delete button is about to do.

I hope I caught all the occurences where one can permanently delete assets.

How to test
-----------
1. Visit e.g. `/trash`
2. If you select some assets, the delete button in the top right corner
   looks different.
2024-06-09 14:25:27 -05:00
Fynn Petersen-Frey
972c66d467 fix(server): proper asset sync (#10019)
* fix(server,mobile): proper asset sync

* fix CI issues

* only use id instead of createdAt+id

* remove createdAt index

* fix typo

* cleanup createdAt usage

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-09 14:19:28 -05:00
Robert Schäfer
69795a3763 refactor: remove dead code from Makefile (#10061)
Motivation
----------
I guess these make targets should have been deleted in 57136e48fb.

How to test
-----------
1. Nothing really, this removes dead code
2024-06-09 19:18:41 +00:00
Robert Schäfer
9c337223e6 ci: automatically apply PR labels (#10064)
Motivation
----------
For me as a new contributor it is frustrating to submit a PR and it will always fail. Even worse: I have to wait for another contributor with more power to assign the label for me.

This will improve developer experience, as some of the labels can be assigned automatically based on changed files.

How to test
-----------
1. Merge this PR
2. Submit a couple of PRs with changes in the respective directories
3. Labels should be automatically applied
4. "Enforce PR labels" github workflow will re-run when "Pull Request Labeler" completes
2024-06-09 14:18:02 -05:00
108 changed files with 2845 additions and 2241 deletions

23
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
cli:
- changed-files:
- any-glob-to-any-file: cli/**
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
🖥web:
- changed-files:
- any-glob-to-any-file: web/**
📱mobile:
- changed-files:
- any-glob-to-any-file: mobile/**
🗄server:
- changed-files:
- any-glob-to-any-file: server/**
🧠machine-learning:
- changed-files:
- any-glob-to-any-file: machine-learning/**

View File

@@ -87,7 +87,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
- name: Build and push image
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v5.4.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@@ -115,7 +115,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v5.4.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}

12
.github/workflows/pr-labeler.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5

View File

@@ -1,13 +0,0 @@
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: Enforce label
runs-on: ubuntu-latest
steps:
- if: toJson(github.event.pull_request.labels) == '[]'
run: exit 1

View File

@@ -10,12 +10,6 @@ dev-update:
dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans

4
cli/package-lock.json generated
View File

@@ -47,14 +47,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},

View File

@@ -3,10 +3,10 @@ global:
evaluation_interval: 15s
scrape_configs:
- job_name: immich_server
- job_name: immich_api
static_configs:
- targets: ['immich-server:8081']
- job_name: immich_microservices
static_configs:
- targets: ['immich-microservices:8081']
- targets: ['immich-server:8082']

View File

@@ -0,0 +1,23 @@
# Email Notifications
Immich supports the option to send notifications via Email for the following events:
- Creating a new user
- Notifying a user when they get added to a shared album
- Informing other users about the addition of new assets to a shared album
## SMTP settings
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
Under Email, enter the following details to connect with SMTP servers.
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
<img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -13,6 +13,20 @@ Immich supports multiple users, each with their own library.
<UserCreate />
## Send new user email notification
:::note
This option is only available if an SMTP server has been configured in the administrator settings.
:::
Admin can send a welcome email if the Email option is set, you can learn here how to set up the SMTP server in Immich.
<img
src={require('./img/send-user-email-notification.webp').default}
width="40%"
title="Send user email notification"
/>
## Set Storage Quota For User
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,20 @@
# SMTP settings using Gmail
This guide walks you through how to get the information you need to set up your Immich instance to send emails using Gmail's SMTP server.
## Create an app password
From your Google account settings
- Add [2-Step Verification](https://support.google.com/accounts/answer/185839) to your Google account (Required)
- [Create an app password](https://myaccount.google.com/apppasswords).
At the end of creating your app passwords, a password will be displayed; save it, it will be used for the password field when setting up the SMTP server in Immich.
<img src={require('./img/google-app-password.webp').default} title="Authorised redirect URIs" />
## Entering the SMTP credential in Immich
Entering your credential in Immich's email notification settings at `Administration -> Settings -> Notification Settings`
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />

View File

@@ -38,17 +38,19 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.106.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
@@ -81,14 +81,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.106.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1,4 +1,11 @@
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -384,6 +391,51 @@ describe('/libraries', () => {
);
});
it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
@@ -507,10 +559,10 @@ describe('/libraries', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
importPaths: [`${testAssetDirInternal}/temp/offline2`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -518,9 +570,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(3);
expect(initialAssets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -541,7 +593,7 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2);
expect(assets.count).toBe(0);
});
it('should not remove online files', async () => {

View File

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

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 140,
"android.injected.version.name" => "1.105.1",
"android.injected.version.code" => 141,
"android.injected.version.name" => "1.106.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')

View File

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

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/forms/login_form.dart';
import 'package:immich_mobile/widgets/forms/login/login_form.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:package_info_plus/package_info_plus.dart';

View File

@@ -101,7 +101,6 @@ class AssetService {
const int chunkSize = 10000;
try {
final List<Asset> allAssets = [];
DateTime? lastCreationDate;
String? lastId;
// will break on error or once all assets are loaded
while (true) {
@@ -109,15 +108,17 @@ class AssetService {
limit: chunkSize,
updatedUntil: until,
lastId: lastId,
lastCreationDate: lastCreationDate,
userId: user.id,
);
log.fine("Requesting $chunkSize assets from $lastId");
final List<AssetResponseDto>? assets =
await _apiService.syncApi.getFullSyncForUser(dto);
if (assets == null) return null;
log.fine(
"Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}",
);
allAssets.addAll(assets.map(Asset.remote));
if (assets.isEmpty) break;
lastCreationDate = assets.last.fileCreatedAt;
if (assets.length != chunkSize) break;
lastId = assets.last.id;
}
return allAssets;

View File

@@ -0,0 +1,17 @@
String? getVersionCompatibilityMessage(
int appMajor,
int appMinor,
int serverMajor,
int serverMinor,
) {
if (serverMajor != appMajor) {
return 'Your app major version is not compatible with the server!';
}
// Add latest compat info up top
if (serverMinor < 106 && appMinor >= 106) {
return 'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login';
}
return null;
}

View File

@@ -0,0 +1,49 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}

View File

@@ -15,11 +15,19 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
import 'package:openapi/api.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget {
@@ -45,9 +53,35 @@ class LoginForm extends HookConsumerWidget {
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
final serverInfo = ref.watch(serverInfoProvider);
final warningMessage = useState<String>('');
final ValueNotifier<String?> serverEndpoint = useState<String?>(null);
checkVersionMismatch() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
final appMajorVersion = int.parse(appVersion.split('.')[0]);
final appMinorVersion = int.parse(appVersion.split('.')[1]);
final serverMajorVersion = serverInfo.serverVersion.major;
final serverMinorVersion = serverInfo.serverVersion.minor;
final message = getVersionCompatibilityMessage(
appMajorVersion,
appMinorVersion,
serverMajorVersion,
serverMinorVersion,
);
if (message != null) {
warningMessage.value = message;
}
} catch (error) {
warningMessage.value = 'Error checking version compatibility';
}
}
/// Fetch the server login credential and enables oAuth login if necessary
/// Returns true if successful, false otherwise
Future<bool> getServerLoginCredential() async {
@@ -308,11 +342,40 @@ class LoginForm extends HookConsumerWidget {
);
}
buildVersionCompatWarning() {
checkVersionMismatch();
if (warningMessage.value.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!,
),
),
child: Text(
warningMessage.value,
textAlign: TextAlign.center,
),
),
);
}
buildLogin() {
return AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildVersionCompatWarning(),
Text(
sanitizeUrl(serverEndpointController.text),
style: context.textTheme.displaySmall,
@@ -416,7 +479,6 @@ class LoginForm extends HookConsumerWidget {
),
],
),
const SizedBox(height: 18),
// Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874
@@ -430,218 +492,3 @@ class LoginForm extends HookConsumerWidget {
);
}
}
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
);
}
}
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/url_helper.dart';
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
),
);
}
}

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.105.1
- API version: 1.106.0
- Generator version: 7.5.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -13,21 +13,12 @@ part of openapi.api;
class AssetFullSyncDto {
/// Returns a new [AssetFullSyncDto] instance.
AssetFullSyncDto({
this.lastCreationDate,
this.lastId,
required this.limit,
required this.updatedUntil,
this.userId,
});
///
/// 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.
///
DateTime? lastCreationDate;
///
/// 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
@@ -51,7 +42,6 @@ class AssetFullSyncDto {
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto &&
other.lastCreationDate == lastCreationDate &&
other.lastId == lastId &&
other.limit == limit &&
other.updatedUntil == updatedUntil &&
@@ -60,22 +50,16 @@ class AssetFullSyncDto {
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(lastCreationDate == null ? 0 : lastCreationDate!.hashCode) +
(lastId == null ? 0 : lastId!.hashCode) +
(limit.hashCode) +
(updatedUntil.hashCode) +
(userId == null ? 0 : userId!.hashCode);
@override
String toString() => 'AssetFullSyncDto[lastCreationDate=$lastCreationDate, lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.lastCreationDate != null) {
json[r'lastCreationDate'] = this.lastCreationDate!.toUtc().toIso8601String();
} else {
// json[r'lastCreationDate'] = null;
}
if (this.lastId != null) {
json[r'lastId'] = this.lastId;
} else {
@@ -99,7 +83,6 @@ class AssetFullSyncDto {
final json = value.cast<String, dynamic>();
return AssetFullSyncDto(
lastCreationDate: mapDateTime(json, r'lastCreationDate', r''),
lastId: mapValueOfType<String>(json, r'lastId'),
limit: mapValueOfType<int>(json, r'limit')!,
updatedUntil: mapDateTime(json, r'updatedUntil', r'')!,

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.105.1+140
version: 1.106.0+141
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -0,0 +1,35 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
void main() {
test('getVersionCompatibilityMessage', () {
String? result;
result = getVersionCompatibilityMessage(1, 0, 2, 0);
expect(
result,
'Your app major version is not compatible with the server!',
);
result = getVersionCompatibilityMessage(1, 106, 1, 105);
expect(
result,
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
);
result = getVersionCompatibilityMessage(1, 107, 1, 105);
expect(
result,
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
);
result = getVersionCompatibilityMessage(1, 106, 1, 106);
expect(result, null);
result = getVersionCompatibilityMessage(1, 107, 1, 106);
expect(result, null);
result = getVersionCompatibilityMessage(1, 107, 1, 108);
expect(result, null);
});
}

View File

@@ -6735,7 +6735,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.105.1",
"version": "1.106.0",
"contact": {}
},
"tags": [],
@@ -7432,10 +7432,6 @@
},
"AssetFullSyncDto": {
"properties": {
"lastCreationDate": {
"format": "date-time",
"type": "string"
},
"lastId": {
"format": "uuid",
"type": "string"

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.105.1
* 1.106.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -904,7 +904,6 @@ export type AssetDeltaSyncResponseDto = {
upserted: AssetResponseDto[];
};
export type AssetFullSyncDto = {
lastCreationDate?: string;
lastId?: string;
limit: number;
updatedUntil: string;

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.105.1",
"version": "1.106.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",
@@ -76,7 +76,6 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
@@ -5790,15 +5789,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
},
"node_modules/@types/imagemin": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
"integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/inquirer": {
"version": "8.2.6",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
@@ -20214,15 +20204,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
},
"@types/imagemin": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
"integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/inquirer": {
"version": "8.2.6",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.105.1",
"version": "1.106.0",
"description": "",
"author": "",
"private": true,
@@ -102,7 +102,6 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",

View File

@@ -374,7 +374,8 @@ export const immichAppConfig: ConfigModuleOptions = {
DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
IMMICH_PORT: Joi.number().optional(),
IMMICH_METRICS_PORT: Joi.number().optional(),
IMMICH_API_METRICS_PORT: Joi.number().optional(),
IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(),
IMMICH_METRICS: Joi.boolean().optional().default(false),
IMMICH_HOST_METRICS: Joi.boolean().optional().default(false),

View File

@@ -127,10 +127,10 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
stack: withStack
? entity.stack?.assets
.filter((a) => a.id !== entity.stack?.primaryAssetId)
.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
?.filter((a) => a.id !== entity.stack?.primaryAssetId)
?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
: undefined,
stackCount: entity.stack?.assets?.length ?? null,
stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null,
isOffline: entity.isOffline,
hasMetadata: true,
duplicateId: entity.duplicateId,

View File

@@ -7,9 +7,6 @@ export class AssetFullSyncDto {
@ValidateUUID({ optional: true })
lastId?: string;
@ValidateDate({ optional: true })
lastCreationDate?: Date;
@ValidateDate()
updatedUntil!: Date;

View File

@@ -16,4 +16,6 @@ export class AssetStackEntity {
@Column({ nullable: false })
primaryAssetId!: string;
assetCount?: number;
}

View File

@@ -122,7 +122,6 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
export interface AssetFullSyncOptions {
ownerId: string;
lastCreationDate?: Date;
lastId?: string;
updatedUntil: Date;
limit: number;

View File

@@ -120,6 +120,10 @@ export interface IEntityJob extends IBaseJob {
source?: 'upload' | 'sidecar-write' | 'copy';
}
export interface IAssetDeleteJob extends IEntityJob {
deleteOnDisk: boolean;
}
export interface ILibraryFileJob extends IEntityJob {
ownerId: string;
assetPath: string;
@@ -246,7 +250,7 @@ export type JobItem =
// Asset Deletion
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
| { name: JobName.ASSET_DELETION; data: IEntityJob }
| { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Management

View File

@@ -1049,50 +1049,18 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
"asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)
AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3)
AND "asset"."updatedAt" <= $4
AND "asset"."id" > $2
AND "asset"."updatedAt" <= $3
ORDER BY
"asset"."fileCreatedAt" DESC,
"asset"."id" DESC
"asset"."id" ASC
LIMIT
10
@@ -1156,42 +1124,11 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
"asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)

View File

@@ -763,36 +763,40 @@ export class AssetRepository implements IAssetRepository {
],
})
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
const { ownerId, lastCreationDate, lastId, updatedUntil, limit } = options;
const { ownerId, lastId, updatedUntil, limit } = options;
const builder = this.getBuilder({
userIds: [ownerId],
exifInfo: true, // also joins stack information
exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit`
withStacked: false, // return all assets individually as expected by the app
});
})
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount');
if (lastCreationDate !== undefined && lastId !== undefined) {
builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', {
lastCreationDate,
lastId,
});
if (lastId !== undefined) {
builder.andWhere('asset.id > :lastId', { lastId });
}
return builder
builder
.andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil })
.orderBy('asset.fileCreatedAt', 'DESC')
.addOrderBy('asset.id', 'DESC')
.limit(limit)
.withDeleted()
.getMany();
.orderBy('asset.id', 'ASC')
.limit(limit) // cannot use `take` for performance reasons
.withDeleted();
return builder.getMany();
}
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] })
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
const builder = this.getBuilder({ userIds: options.userIds, exifInfo: true, withStacked: false })
const builder = this.getBuilder({
userIds: options.userIds,
exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit`
withStacked: false, // return all assets individually as expected by the app
})
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount')
.andWhere({ updatedAt: MoreThan(options.updatedAfter) })
.limit(options.limit)
.limit(options.limit) // cannot use `take` for performance reasons
.withDeleted();
return builder.getMany();
}
}

View File

@@ -389,8 +389,8 @@ describe(AssetService.name, () => {
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: 'asset1' } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset2' } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } },
]);
});
@@ -410,7 +410,7 @@ describe(AssetService.name, () => {
assetMock.getById.mockResolvedValue(assetWithFace);
await sut.handleAssetDeletion({ id: assetWithFace.id });
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
expect(jobMock.queue.mock.calls).toEqual([
[
@@ -435,7 +435,7 @@ describe(AssetService.name, () => {
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
assetMock.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
expect(assetStackMock.update).toHaveBeenCalledWith({
id: 'stack-1',
@@ -446,10 +446,21 @@ describe(AssetService.name, () => {
it('should delete a live photo', async () => {
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id });
await sut.handleAssetDeletion({
id: assetStub.livePhotoStillAsset.id,
deleteOnDisk: true,
});
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.ASSET_DELETION, data: { id: assetStub.livePhotoMotionAsset.id } }],
[
{
name: JobName.ASSET_DELETION,
data: {
id: assetStub.livePhotoMotionAsset.id,
deleteOnDisk: true,
},
},
],
[
{
name: JobName.DELETE_FILES,
@@ -463,7 +474,7 @@ describe(AssetService.name, () => {
it('should update usage', async () => {
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.handleAssetDeletion({ id: assetStub.image.id });
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
});
});

View File

@@ -27,7 +27,7 @@ import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import {
IEntityJob,
IAssetDeleteJob,
IJobRepository,
ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE,
@@ -256,15 +256,21 @@ export class AssetService {
for await (const assets of assetPagination) {
await this.jobRepository.queueAll(
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
assets.map((asset) => ({
name: JobName.ASSET_DELETION,
data: {
id: asset.id,
deleteOnDisk: true,
},
})),
);
}
return JobStatus.SUCCESS;
}
async handleAssetDeletion(job: IEntityJob): Promise<JobStatus> {
const { id } = job;
async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
const { id, deleteOnDisk } = job;
const asset = await this.assetRepository.getById(id, {
faces: {
@@ -301,12 +307,14 @@ export class AssetService {
// TODO refactor this to use cascades
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
await this.jobRepository.queue({
name: JobName.ASSET_DELETION,
data: { id: asset.livePhotoVideoId, deleteOnDisk },
});
}
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
// skip originals if the user deleted the whole library
if (!asset.library?.deletedAt) {
if (deleteOnDisk) {
files.push(asset.sidecarPath, asset.originalPath);
}
@@ -321,7 +329,12 @@ export class AssetService {
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
if (force) {
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } })));
await this.jobRepository.queueAll(
ids.map((id) => ({
name: JobName.ASSET_DELETION,
data: { id, deleteOnDisk: true },
})),
);
} else {
await this.assetRepository.softDeleteAll(ids);
this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, ids);

View File

@@ -1276,7 +1276,7 @@ describe(LibraryService.name, () => {
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id } },
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } },
]);
});
});

View File

@@ -355,7 +355,13 @@ export class LibraryService {
const assetIds = await this.repository.getAssetIds(job.id, true);
this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`);
await this.jobRepository.queueAll(
assetIds.map((assetId) => ({ name: JobName.ASSET_DELETION, data: { id: assetId } })),
assetIds.map((assetId) => ({
name: JobName.ASSET_DELETION,
data: {
id: assetId,
deleteOnDisk: false,
},
})),
);
if (assetIds.length === 0) {
@@ -544,7 +550,13 @@ export class LibraryService {
for await (const assets of assetPagination) {
this.logger.debug(`Removing ${assets.length} offline assets`);
await this.jobRepository.queueAll(
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
assets.map((asset) => ({
name: JobName.ASSET_DELETION,
data: {
id: asset.id,
deleteOnDisk: false,
},
})),
);
}

View File

@@ -510,7 +510,7 @@ describe(MetadataService.name, () => {
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
expect(jobMock.queue).toHaveBeenNthCalledWith(1, {
name: JobName.ASSET_DELETION,
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId },
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true },
});
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
name: JobName.METADATA_EXTRACTION,

View File

@@ -460,7 +460,10 @@ export class MetadataService {
// (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId)
// note asset.livePhotoVideoId is not motionAsset.id yet
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
await this.jobRepository.queue({
name: JobName.ASSET_DELETION,
data: { id: asset.livePhotoVideoId, deleteOnDisk: true },
});
this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);
}
}

View File

@@ -17,7 +17,7 @@ import { StorageService } from 'src/services/storage.service';
import { SystemConfigService } from 'src/services/system-config.service';
import { UserService } from 'src/services/user.service';
import { VersionService } from 'src/services/version.service';
import { otelSDK } from 'src/utils/instrumentation';
import { otelShutdown } from 'src/utils/instrumentation';
@Injectable()
export class MicroservicesService {
@@ -102,6 +102,6 @@ export class MicroservicesService {
async teardown() {
await this.libraryService.teardown();
await this.metadataService.teardown();
await otelSDK.shutdown();
await otelShutdown();
}
}

View File

@@ -32,7 +32,6 @@ export class SyncService {
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId,
lastCreationDate: dto.lastCreationDate,
updatedUntil: dto.updatedUntil,
lastId: dto.lastId,
limit: dto.limit,

View File

@@ -79,7 +79,7 @@ describe(TrashService.name, () => {
assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
await expect(sut.empty(authStub.user1)).resolves.toBeUndefined();
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id } },
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
]);
});
});

View File

@@ -49,7 +49,13 @@ export class TrashService {
for await (const assets of assetPagination) {
await this.jobRepository.queueAll(
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
assets.map((asset) => ({
name: JobName.ASSET_DELETION,
data: {
id: asset.id,
deleteOnDisk: true,
},
})),
);
}
}

View File

@@ -33,23 +33,36 @@ const aggregation = new metrics.ExplicitBucketHistogramAggregation(
true,
);
const metricsPort = Number.parseInt(process.env.IMMICH_METRICS_PORT ?? '8081');
let otelSingleton: NodeSDK | undefined;
export const otelSDK = new NodeSDK({
resource: new resources.Resource({
[SemanticResourceAttributes.SERVICE_NAME]: `immich`,
[SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
}),
metricReader: new PrometheusExporter({ port: metricsPort }),
contextManager: new AsyncLocalStorageContextManager(),
instrumentations: [
new HttpInstrumentation(),
new IORedisInstrumentation(),
new NestInstrumentation(),
new PgInstrumentation(),
],
views: [new metrics.View({ aggregation, instrumentName: '*', instrumentUnit: 'ms' })],
});
export const otelStart = (port: number) => {
if (otelSingleton) {
throw new Error('OpenTelemetry SDK already started');
}
otelSingleton = new NodeSDK({
resource: new resources.Resource({
[SemanticResourceAttributes.SERVICE_NAME]: `immich`,
[SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
}),
metricReader: new PrometheusExporter({ port }),
contextManager: new AsyncLocalStorageContextManager(),
instrumentations: [
new HttpInstrumentation(),
new IORedisInstrumentation(),
new NestInstrumentation(),
new PgInstrumentation(),
],
views: [new metrics.View({ aggregation, instrumentName: '*', instrumentUnit: 'ms' })],
});
otelSingleton.start();
};
export const otelShutdown = async () => {
if (otelSingleton) {
await otelSingleton.shutdown();
otelSingleton = undefined;
}
};
export const otelConfig: OpenTelemetryModuleOptions = {
metrics: {

View File

@@ -9,14 +9,16 @@ import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/const
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { ApiService } from 'src/services/api.service';
import { otelSDK } from 'src/utils/instrumentation';
import { otelStart } from 'src/utils/instrumentation';
import { useSwagger } from 'src/utils/misc';
const host = process.env.HOST;
async function bootstrap() {
process.title = 'immich-api';
otelSDK.start();
const otelPort = Number.parseInt(process.env.IMMICH_API_METRICS_PORT ?? '8081');
otelStart(otelPort);
const port = Number(process.env.IMMICH_PORT) || 3001;
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });

View File

@@ -4,10 +4,12 @@ import { MicroservicesModule } from 'src/app.module';
import { envName, serverVersion } from 'src/constants';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { otelSDK } from 'src/utils/instrumentation';
import { otelStart } from 'src/utils/instrumentation';
export async function bootstrap() {
otelSDK.start();
const otelPort = Number.parseInt(process.env.IMMICH_MICROSERVICES_METRICS_PORT ?? '8082');
otelStart(otelPort);
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
const logger = await app.resolve(ILoggerRepository);

View File

@@ -8,7 +8,6 @@ node_modules
.env.*
!.env.example
*.md
*.json
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml

View File

@@ -1,9 +1,9 @@
{
"singleQuote": true,
"trailingComma": "all",
"organizeImportsSkipDestructiveCodeActions": true,
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-svelte", "prettier-plugin-sort-json"],
"printWidth": 120,
"semi": true,
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-svelte"],
"organizeImportsSkipDestructiveCodeActions": true,
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"singleQuote": true,
"trailingComma": "all"
}

19
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.105.1",
"version": "1.106.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
@@ -54,6 +54,7 @@
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.12",
@@ -67,7 +68,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
@@ -7115,6 +7116,18 @@
}
}
},
"node_modules/prettier-plugin-sort-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.0.0.tgz",
"integrity": "sha512-zV5g+bWFD2zAqyQ8gCkwUTC49o9FxslaUdirwivt5GZHcf57hCocavykuyYqbExoEsuBOg8IU36OY7zmVEMOWA==",
"dev": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.105.1",
"version": "1.106.0",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -48,6 +48,7 @@
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.12",
@@ -74,8 +75,8 @@
"lodash-es": "^4.17.21",
"luxon": "^3.4.4",
"socket.io-client": "^4.7.4",
"svelte-local-storage-store": "^0.6.4",
"svelte-i18n": "^4.0.0",
"svelte-local-storage-store": "^0.6.4",
"svelte-maplibre": "^0.9.0",
"thumbhash": "^0.1.1"
},

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { mdiDeleteOutline } from '@mdi/js';
import { mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js';
import { type AssetResponseDto } from '@immich/sdk';
export let asset: AssetResponseDto;
@@ -18,7 +18,7 @@
{#if asset.isTrashed}
<CircleIconButton
color="opaque"
icon={mdiDeleteOutline}
icon={mdiDeleteForeverOutline}
on:click={() => dispatch('permanentlyDelete')}
title={$t('permanently_delete')}
/>

View File

@@ -106,7 +106,6 @@
</script>
<svelte:window
on:wheel|preventDefault|nonpassive
use:shortcuts={[
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut, preventDefault: false },
{ shortcut: { key: 'c', meta: true }, onShortcut: onCopyShortcut, preventDefault: false },

View File

@@ -3,7 +3,7 @@
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js';
import { mdiTimerSand, mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js';
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
import { t } from 'svelte-i18n';
@@ -43,7 +43,7 @@
{:else if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
{:else}
<CircleIconButton title={label} icon={mdiDeleteOutline} on:click={handleTrash} />
<CircleIconButton title={label} icon={mdiDeleteForeverOutline} on:click={handleTrash} />
{/if}
{#if isShowConfirmation}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -1,8 +1,8 @@
{
"account": "Konto",
"acknowledge": "",
"action": "",
"actions": "",
"acknowledge": "Bestätigen",
"action": "Aktion",
"actions": "Aktionen",
"active": "Aktiv",
"activity": "Aktivität",
"add": "Hinzufügen",
@@ -10,25 +10,25 @@
"add_a_location": "Standort hinzufügen",
"add_a_name": "Name hinzufügen",
"add_a_title": "Titel hinzufügen",
"add_exclusion_pattern": "",
"add_import_path": "",
"add_location": "",
"add_exclusion_pattern": "Ausschlussmuster hinzufügen",
"add_import_path": "Importpfad hinzufügen",
"add_location": "Ort hinzufügen",
"add_more_users": "",
"add_partner": "",
"add_path": "",
"add_photos": "",
"add_to": "Hinzufügen zu...",
"add_to_album": "",
"add_to_shared_album": "",
"add_partner": "Partner hinzufügen",
"add_path": "Pfad hinzufügen",
"add_photos": "Fotos hinzufügen",
"add_to": "Hinzufügen zu ...",
"add_to_album": "Zu Album hinzufügen",
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
"admin": {
"authentication_settings": "",
"authentication_settings_description": "",
"crontab_guru": "",
"disable_login": "",
"authentication_settings": "Authentifizierungseinstellungen",
"authentication_settings_description": "Verwalten von Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen",
"crontab_guru": "Crontab Guru",
"disable_login": "Login deaktvieren",
"disabled": "Deaktiviert",
"duplicate_detection_job_description": "",
"image_format_description": "",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
@@ -218,19 +218,27 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"albums": "",
"all": "",
"all_people": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "Alben",
"all": "Alle",
"all_people": "Alle Personen",
"allow_dark_mode": "",
"allow_edits": "",
"api_key": "",
@@ -246,11 +254,11 @@
"back": "",
"backward": "",
"blurred_background": "",
"camera": "",
"camera": "Kamera",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cancel": "Abbrechen",
"cancel_search": "Suche abbrechen",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"cant_apply_changes": "",
@@ -266,17 +274,17 @@
"change_your_password": "",
"changed_visibility_successfully": "",
"check_logs": "",
"city": "",
"city": "Stadt",
"clear": "",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"close": "",
"close": "Schliessen",
"collapse_all": "",
"color_theme": "",
"comment_options": "",
"comments_are_disabled": "",
"confirm": "",
"confirm": "Bestätigen",
"confirm_admin_password": "",
"confirm_password": "",
"contain": "",
@@ -290,17 +298,17 @@
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "",
"country": "Land",
"cover": "",
"covers": "",
"create": "",
"create_album": "",
"create_library": "",
"create_link": "",
"create": "Erstellen",
"create_album": "Album erstellen",
"create_library": "Bibliothek erstellen",
"create_link": "Link erstellen",
"create_link_to_share": "",
"create_new_person": "",
"create_new_user": "",
"create_user": "",
"create_new_user": "Neuen Nutzer erstellen",
"create_user": "Nutzer erstellen",
"created": "",
"current_device": "",
"custom_locale": "",
@@ -310,10 +318,10 @@
"date_and_time": "",
"date_before": "",
"date_range": "",
"day": "",
"day": "Tag",
"default_locale": "",
"default_locale_description": "",
"delete": "",
"delete": "Löschen",
"delete_album": "",
"delete_key": "",
"delete_library": "",
@@ -321,7 +329,7 @@
"delete_shared_link": "",
"delete_user": "",
"deleted_shared_link": "",
"description": "",
"description": "Beschreibung",
"details": "",
"direction": "",
"disallow_edits": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -490,6 +490,8 @@
"jobs": "Jobs",
"keep": "Keep",
"keyboard_shortcuts": "Keyboard shortcuts",
"language": "Language",
"language_setting_description": "Select your preferred language",
"last_seen": "Last seen",
"leave": "Leave",
"let_others_respond": "Let others respond",
@@ -734,6 +736,7 @@
"template": "Template",
"theme": "Theme",
"theme_selection": "Theme selection",
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
"time_based_memories": "Time-based memories",
"timezone": "Timezone",
"toggle_settings": "Toggle settings",
@@ -784,8 +787,5 @@
"welcome_to_immich": "Welcome to immich",
"year": "Year",
"yes": "Yes",
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
"language_setting_description": "Select your preferred language",
"language": "Language",
"zoom_image": "Zoom Image"
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": "",
"notification_email_test_email_failed": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_failed": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -5,7 +5,7 @@
"actions": "",
"active": "",
"activity": "",
"add": "",
"add": "Hozzáadás",
"add_a_description": "",
"add_a_location": "",
"add_a_name": "",
@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_sent": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_failed": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"library_tasks_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"library_tasks_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_test_email_sent": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"library_tasks_description": "",
"notification_email_test_email_sent": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

View File

@@ -218,16 +218,24 @@
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"video_conversion_job_description": "",
"notification_email_test_email_failed": "",
"notification_email_sent_test_email_button": "",
"library_tasks_description": "",
"notification_email_test_email_sent": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"all": "",
"all_people": "",
@@ -483,6 +491,8 @@
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
@@ -552,6 +562,10 @@
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"ok": "",
"oldest_first": "",
@@ -724,6 +738,7 @@
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"toggle_settings": "",
@@ -774,8 +789,5 @@
"welcome_to_immich": "",
"year": "",
"yes": "",
"theme_selection_description": "",
"language_setting_description": "",
"language": "",
"zoom_image": ""
}

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