Compare commits
20 Commits
chore/hand
...
v1.106.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f32c02bd25 | ||
|
|
b16c9405d8 | ||
|
|
46df165ef2 | ||
|
|
19e35d8d3f | ||
|
|
c4c070569f | ||
|
|
7651f70c88 | ||
|
|
4698c39855 | ||
|
|
2f2aecfb47 | ||
|
|
20efd82461 | ||
|
|
22a0b4d900 | ||
|
|
2f25a8a437 | ||
|
|
7a0bc0ea87 | ||
|
|
a564c80193 | ||
|
|
f4671617d1 | ||
|
|
d331da0ced | ||
|
|
84da9abcbc | ||
|
|
48eede59b9 | ||
|
|
972c66d467 | ||
|
|
69795a3763 | ||
|
|
9c337223e6 |
23
.github/labeler.yml
vendored
Normal file
23
.github/labeler.yml
vendored
Normal 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/**
|
||||
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -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
12
.github/workflows/pr-labeler.yml
vendored
Normal 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
|
||||
13
.github/workflows/pr-require-label.yml
vendored
13
.github/workflows/pr-require-label.yml
vendored
@@ -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
|
||||
|
||||
6
Makefile
6
Makefile
@@ -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
4
cli/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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']
|
||||
|
||||
23
docs/docs/administration/email-notification.mdx
Normal file
23
docs/docs/administration/email-notification.mdx
Normal 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" />
|
||||
BIN
docs/docs/administration/img/email-settings.png
Normal file
BIN
docs/docs/administration/img/email-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
BIN
docs/docs/administration/img/immich-email-notefaction.webp
Normal file
BIN
docs/docs/administration/img/immich-email-notefaction.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/docs/administration/img/send-user-email-notification.webp
Normal file
BIN
docs/docs/administration/img/send-user-email-notification.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/docs/administration/img/user-notifications-settings.png
Normal file
BIN
docs/docs/administration/img/user-notifications-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -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.
|
||||
|
||||
BIN
docs/docs/guides/img/email-settings.png
Normal file
BIN
docs/docs/guides/img/email-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
BIN
docs/docs/guides/img/google-app-password.webp
Normal file
BIN
docs/docs/guides/img/google-app-password.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
20
docs/docs/guides/smtp-gmail.md
Normal file
20
docs/docs/guides/smtp-gmail.md
Normal 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" />
|
||||
@@ -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
8
e2e/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.105.1",
|
||||
"version": "1.106.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
17
mobile/lib/utils/version_compatibility.dart
Normal file
17
mobile/lib/utils/version_compatibility.dart
Normal 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;
|
||||
}
|
||||
49
mobile/lib/widgets/forms/login/email_input.dart
Normal file
49
mobile/lib/widgets/forms/login/email_input.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
mobile/lib/widgets/forms/login/loading_icon.dart
Normal file
21
mobile/lib/widgets/forms/login/loading_icon.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
mobile/lib/widgets/forms/login/login_button.dart
Normal file
27
mobile/lib/widgets/forms/login/login_button.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
mobile/lib/widgets/forms/login/o_auth_login_button.dart
Normal file
34
mobile/lib/widgets/forms/login/o_auth_login_button.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
mobile/lib/widgets/forms/login/password_input.dart
Normal file
49
mobile/lib/widgets/forms/login/password_input.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
54
mobile/lib/widgets/forms/login/server_endpoint_input.dart
Normal file
54
mobile/lib/widgets/forms/login/server_endpoint_input.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -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
|
||||
|
||||
|
||||
19
mobile/openapi/lib/model/asset_full_sync_dto.dart
generated
19
mobile/openapi/lib/model/asset_full_sync_dto.dart
generated
@@ -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'')!,
|
||||
|
||||
@@ -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'
|
||||
|
||||
35
mobile/test/modules/utils/version_compatibility_test.dart
Normal file
35
mobile/test/modules/utils/version_compatibility_test.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
23
server/package-lock.json
generated
23
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,9 +7,6 @@ export class AssetFullSyncDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
lastId?: string;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
lastCreationDate?: Date;
|
||||
|
||||
@ValidateDate()
|
||||
updatedUntil!: Date;
|
||||
|
||||
|
||||
@@ -16,4 +16,6 @@ export class AssetStackEntity {
|
||||
|
||||
@Column({ nullable: false })
|
||||
primaryAssetId!: string;
|
||||
|
||||
assetCount?: number;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,6 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
|
||||
|
||||
export interface AssetFullSyncOptions {
|
||||
ownerId: string;
|
||||
lastCreationDate?: Date;
|
||||
lastId?: string;
|
||||
updatedUntil: Date;
|
||||
limit: number;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,7 +8,6 @@ node_modules
|
||||
.env.*
|
||||
!.env.example
|
||||
*.md
|
||||
*.json
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -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
19
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user