Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d2a849edc | |||
| fb4fe5d40b | |||
| 717961ce7b | |||
| 259386cf13 | |||
| 7e587c2703 |
@@ -1,23 +0,0 @@
|
|||||||
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/**
|
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.4.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.4.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.context }}
|
context: ${{ matrix.context }}
|
||||||
file: ${{ matrix.file }}
|
file: ${{ matrix.file }}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@@ -10,6 +10,12 @@ dev-update:
|
|||||||
dev-scale:
|
dev-scale:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
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
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|||||||
Generated
+2
-2
@@ -47,14 +47,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.12.13",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ global:
|
|||||||
evaluation_interval: 15s
|
evaluation_interval: 15s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: immich_api
|
- job_name: immich_server
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8081']
|
- targets: ['immich-server:8081']
|
||||||
|
|
||||||
- job_name: immich_microservices
|
- job_name: immich_microservices
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8082']
|
- targets: ['immich-microservices:8081']
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Email Notifications
|
|
||||||
|
|
||||||
Immich supports the option to send notifications via Email for the following events:
|
|
||||||
|
|
||||||
- Creating a new user
|
|
||||||
- Notifying a user when they get added to a shared album
|
|
||||||
- Informing other users about the addition of new assets to a shared album
|
|
||||||
|
|
||||||
## SMTP settings
|
|
||||||
|
|
||||||
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
|
|
||||||
|
|
||||||
Under Email, enter the following details to connect with SMTP servers.
|
|
||||||
|
|
||||||
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
|
|
||||||
|
|
||||||
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
|
|
||||||
|
|
||||||
## User's notifications settings
|
|
||||||
|
|
||||||
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
|
|
||||||
|
|
||||||
<img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
@@ -13,20 +13,6 @@ Immich supports multiple users, each with their own library.
|
|||||||
|
|
||||||
<UserCreate />
|
<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
|
## 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.
|
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,20 +0,0 @@
|
|||||||
# 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" />
|
|
||||||
@@ -39,7 +39,7 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
|||||||
## General
|
## General
|
||||||
|
|
||||||
| Variable | Description | Default | Containers | Workers |
|
| Variable | Description | Default | Containers | Workers |
|
||||||
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
| :------------------------------ | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | | server | microservices |
|
| `TZ` | Timezone | | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, 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_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
@@ -49,8 +49,6 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
|||||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
|
| `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 | |
|
| `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 | |
|
| `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`.
|
\*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.
|
It only need to be set if the Immich deployment method is changing.
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
@@ -81,14 +81,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.12.13",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
||||||
LibraryResponseDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
ScanLibraryDto,
|
|
||||||
getAllLibraries,
|
|
||||||
removeOfflineFiles,
|
|
||||||
scanLibrary,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { cpSync, existsSync } from 'node:fs';
|
import { cpSync, existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
@@ -391,51 +384,6 @@ 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 () => {
|
it('should scan new files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
@@ -559,10 +507,10 @@ describe('/libraries', () => {
|
|||||||
it('should remove offline files', async () => {
|
it('should remove offline files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline2`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
|
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
@@ -570,9 +518,9 @@ describe('/libraries', () => {
|
|||||||
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
|
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
});
|
});
|
||||||
expect(initialAssets.count).toBe(1);
|
expect(initialAssets.count).toBe(3);
|
||||||
|
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
|
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
@@ -593,7 +541,7 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
expect(assets.count).toBe(0);
|
expect(assets.count).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remove online files', async () => {
|
it('should not remove online files', async () => {
|
||||||
|
|||||||
@@ -1,40 +1,33 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from insightface.model_zoo import RetinaFace
|
import onnxruntime as ort
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from app.models.base import InferenceModel
|
from app.models.base import InferenceModel
|
||||||
from app.models.transforms import decode_cv2
|
from app.models.session import ort_has_batch_dim, ort_expand_outputs
|
||||||
|
from app.models.transforms import decode_pil
|
||||||
from app.schemas import FaceDetectionOutput, ModelSession, ModelTask, ModelType
|
from app.schemas import FaceDetectionOutput, ModelSession, ModelTask, ModelType
|
||||||
|
from .scrfd import SCRFD
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.ImageOps import pad
|
||||||
|
|
||||||
class FaceDetector(InferenceModel):
|
class FaceDetector(InferenceModel):
|
||||||
depends = []
|
depends = []
|
||||||
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
|
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
model_name: str,
|
|
||||||
min_score: float = 0.7,
|
|
||||||
cache_dir: Path | str | None = None,
|
|
||||||
**model_kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
self.min_score = model_kwargs.pop("minScore", min_score)
|
|
||||||
super().__init__(model_name, cache_dir, **model_kwargs)
|
|
||||||
|
|
||||||
def _load(self) -> ModelSession:
|
def _load(self) -> ModelSession:
|
||||||
session = self._make_session(self.model_path)
|
session = self._make_session(self.model_path)
|
||||||
self.model = RetinaFace(session=session)
|
if isinstance(session, ort.InferenceSession) and not ort_has_batch_dim(session):
|
||||||
self.model.prepare(ctx_id=0, det_thresh=self.min_score, input_size=(640, 640))
|
ort_expand_outputs(session)
|
||||||
|
self.model = SCRFD(session=session)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
def _predict(self, inputs: NDArray[np.uint8] | bytes, **kwargs: Any) -> FaceDetectionOutput:
|
def _predict(self, inputs: NDArray[np.uint8] | bytes | Image.Image, **kwargs: Any) -> FaceDetectionOutput:
|
||||||
inputs = decode_cv2(inputs)
|
inputs = self._transform(inputs)
|
||||||
|
|
||||||
bboxes, landmarks = self._detect(inputs)
|
[bboxes], [landmarks] = self.model.detect(inputs, threshold=kwargs.pop("minScore", 0.7))
|
||||||
return {
|
return {
|
||||||
"boxes": bboxes[:, :4].round(),
|
"boxes": bboxes[:, :4].round(),
|
||||||
"scores": bboxes[:, 4],
|
"scores": bboxes[:, 4],
|
||||||
@@ -44,5 +37,7 @@ class FaceDetector(InferenceModel):
|
|||||||
def _detect(self, inputs: NDArray[np.uint8] | bytes) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
def _detect(self, inputs: NDArray[np.uint8] | bytes) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
||||||
return self.model.detect(inputs) # type: ignore
|
return self.model.detect(inputs) # type: ignore
|
||||||
|
|
||||||
def configure(self, **kwargs: Any) -> None:
|
def _transform(self, inputs: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]:
|
||||||
self.model.det_thresh = kwargs.pop("minScore", self.model.det_thresh)
|
image = decode_pil(inputs)
|
||||||
|
padded = pad(image, (640, 640), method=Image.Resampling.BICUBIC)
|
||||||
|
return np.array(padded, dtype=np.uint8)[None, ...]
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import onnx
|
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from insightface.model_zoo import ArcFaceONNX
|
from insightface.model_zoo import ArcFaceONNX
|
||||||
from insightface.utils.face_align import norm_crop
|
from insightface.utils.face_align import norm_crop
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from onnx.tools.update_model_dims import update_inputs_outputs_dims
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from app.config import clean_name, log
|
from app.config import clean_name, log
|
||||||
from app.models.base import InferenceModel
|
from app.models.base import InferenceModel
|
||||||
|
from app.models.session import ort_add_batch_dim, ort_has_batch_dim
|
||||||
from app.models.transforms import decode_cv2
|
from app.models.transforms import decode_cv2
|
||||||
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelSession, ModelTask, ModelType
|
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelSession, ModelTask, ModelType
|
||||||
|
|
||||||
@@ -32,8 +31,9 @@ class FaceRecognizer(InferenceModel):
|
|||||||
|
|
||||||
def _load(self) -> ModelSession:
|
def _load(self) -> ModelSession:
|
||||||
session = self._make_session(self.model_path)
|
session = self._make_session(self.model_path)
|
||||||
if not self._has_batch_dim(session):
|
if isinstance(session, ort.InferenceSession) and not ort_has_batch_dim(session):
|
||||||
self._add_batch_dim(self.model_path)
|
log.info(f"Adding batch dimension to recognition model {self.model_name}")
|
||||||
|
ort_add_batch_dim(self.model_path, self.model_path)
|
||||||
session = self._make_session(self.model_path)
|
session = self._make_session(self.model_path)
|
||||||
self.model = ArcFaceONNX(
|
self.model = ArcFaceONNX(
|
||||||
self.model_path.with_suffix(".onnx").as_posix(),
|
self.model_path.with_suffix(".onnx").as_posix(),
|
||||||
@@ -62,16 +62,3 @@ class FaceRecognizer(InferenceModel):
|
|||||||
|
|
||||||
def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]:
|
def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]:
|
||||||
return [norm_crop(image, landmark) for landmark in faces["landmarks"]]
|
return [norm_crop(image, landmark) for landmark in faces["landmarks"]]
|
||||||
|
|
||||||
def _has_batch_dim(self, session: ort.InferenceSession) -> bool:
|
|
||||||
return not isinstance(session, ort.InferenceSession) or session.get_inputs()[0].shape[0] == "batch"
|
|
||||||
|
|
||||||
def _add_batch_dim(self, model_path: Path) -> None:
|
|
||||||
log.debug(f"Adding batch dimension to model {model_path}")
|
|
||||||
proto = onnx.load(model_path)
|
|
||||||
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
|
|
||||||
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]
|
|
||||||
input_dims = {proto.graph.input[0].name: ["batch"] + static_input_dims}
|
|
||||||
output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
|
|
||||||
updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
|
|
||||||
onnx.save(updated_proto, model_path)
|
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
# Based on InsightFace-REST by SthPhoenix https://github.com/SthPhoenix/InsightFace-REST/blob/master/src/api_trt/modules/model_zoo/detectors/scrfd.py
|
||||||
|
# Primary changes made:
|
||||||
|
# 1. Removed CuPy-related code
|
||||||
|
# 2. Adapted proposal generation to be thread-safe
|
||||||
|
# 3. Added typing
|
||||||
|
# 4. Assume RGB input
|
||||||
|
# 5. Removed unused variables
|
||||||
|
|
||||||
|
# Copyright 2021 SthPhoenix
|
||||||
|
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Based on Jia Guo reference implementation at
|
||||||
|
# https://github.com/deepinsight/insightface/blob/master/detection/scrfd/tools/scrfd.py
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from numba import njit
|
||||||
|
from app.schemas import ModelSession
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True, nogil=True)
|
||||||
|
def nms(dets, threshold: float = 0.4) -> NDArray[np.float32]:
|
||||||
|
x1 = dets[:, 0]
|
||||||
|
y1 = dets[:, 1]
|
||||||
|
x2 = dets[:, 2]
|
||||||
|
y2 = dets[:, 3]
|
||||||
|
scores = dets[:, 4]
|
||||||
|
|
||||||
|
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
||||||
|
order = scores.argsort()[::-1]
|
||||||
|
|
||||||
|
keep = []
|
||||||
|
while order.size > 0:
|
||||||
|
i = order[0]
|
||||||
|
keep.append(i)
|
||||||
|
xx1 = np.maximum(x1[i], x1[order[1:]])
|
||||||
|
yy1 = np.maximum(y1[i], y1[order[1:]])
|
||||||
|
xx2 = np.minimum(x2[i], x2[order[1:]])
|
||||||
|
yy2 = np.minimum(y2[i], y2[order[1:]])
|
||||||
|
|
||||||
|
w = np.maximum(0.0, xx2 - xx1 + 1)
|
||||||
|
h = np.maximum(0.0, yy2 - yy1 + 1)
|
||||||
|
inter = w * h
|
||||||
|
ovr = inter / (areas[i] + areas[order[1:]] - inter)
|
||||||
|
|
||||||
|
inds = np.where(ovr <= threshold)[0]
|
||||||
|
order = order[inds + 1]
|
||||||
|
|
||||||
|
return np.asarray(keep)
|
||||||
|
|
||||||
|
|
||||||
|
@njit(fastmath=True, cache=True, nogil=True)
|
||||||
|
def single_distance2bbox(point: NDArray[np.float32], distance: NDArray[np.float32], stride: int) -> NDArray[np.float32]:
|
||||||
|
"""
|
||||||
|
Fast conversion of single bbox distances to coordinates
|
||||||
|
|
||||||
|
:param point: Anchor point
|
||||||
|
:param distance: Bbox distances from anchor point
|
||||||
|
:param stride: Current stride scale
|
||||||
|
:return: bbox
|
||||||
|
"""
|
||||||
|
distance[0] = point[0] - distance[0] * stride
|
||||||
|
distance[1] = point[1] - distance[1] * stride
|
||||||
|
distance[2] = point[0] + distance[2] * stride
|
||||||
|
distance[3] = point[1] + distance[3] * stride
|
||||||
|
return distance
|
||||||
|
|
||||||
|
|
||||||
|
@njit(fastmath=True, cache=True, nogil=True)
|
||||||
|
def single_distance2kps(point: NDArray[np.float32], distance: NDArray[np.float32], stride: int) -> NDArray[np.float32]:
|
||||||
|
"""
|
||||||
|
Fast conversion of single keypoint distances to coordinates
|
||||||
|
|
||||||
|
:param point: Anchor point
|
||||||
|
:param distance: Keypoint distances from anchor point
|
||||||
|
:param stride: Current stride scale
|
||||||
|
:return: keypoint
|
||||||
|
"""
|
||||||
|
for ix in range(0, distance.shape[0], 2):
|
||||||
|
distance[ix] = distance[ix] * stride + point[0]
|
||||||
|
distance[ix + 1] = distance[ix + 1] * stride + point[1]
|
||||||
|
return distance
|
||||||
|
|
||||||
|
|
||||||
|
@njit(fastmath=True, cache=True, nogil=True)
|
||||||
|
def generate_proposals(
|
||||||
|
score_blob: NDArray[np.float32],
|
||||||
|
bbox_blob: NDArray[np.float32],
|
||||||
|
kpss_blob: NDArray[np.float32],
|
||||||
|
stride: int,
|
||||||
|
anchors: NDArray[np.float32],
|
||||||
|
threshold: float,
|
||||||
|
) -> tuple[NDArray[np.float32], NDArray[np.float32], NDArray[np.float32]]:
|
||||||
|
"""
|
||||||
|
Convert distances from anchors to actual coordinates on source image
|
||||||
|
and filter proposals by confidence threshold.
|
||||||
|
|
||||||
|
:param score_blob: Raw scores for stride
|
||||||
|
:param bbox_blob: Raw bbox distances for stride
|
||||||
|
:param kpss_blob: Raw keypoints distances for stride
|
||||||
|
:param stride: Stride scale
|
||||||
|
:param anchors: Precomputed anchors for stride
|
||||||
|
:param threshold: Confidence threshold
|
||||||
|
:return: Filtered scores, bboxes and keypoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
idxs = []
|
||||||
|
for ix in range(score_blob.shape[0]):
|
||||||
|
if score_blob[ix][0] > threshold:
|
||||||
|
idxs.append(ix)
|
||||||
|
|
||||||
|
score_out = np.empty((len(idxs), 1), dtype="float32")
|
||||||
|
bbox_out = np.empty((len(idxs), 4), dtype="float32")
|
||||||
|
kpss_out = np.empty((len(idxs), 10), dtype="float32")
|
||||||
|
|
||||||
|
for i in range(len(idxs)):
|
||||||
|
ix = idxs[i]
|
||||||
|
score_out[i] = score_blob[ix]
|
||||||
|
bbox_out[i] = single_distance2bbox(anchors[ix], bbox_blob[ix], stride)
|
||||||
|
kpss_out[i] = single_distance2kps(anchors[ix], kpss_blob[ix], stride)
|
||||||
|
|
||||||
|
return score_out, bbox_out, kpss_out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(fastmath=True, cache=True, nogil=True)
|
||||||
|
def filter(
|
||||||
|
bboxes_list: NDArray[np.float32],
|
||||||
|
kpss_list: NDArray[np.float32],
|
||||||
|
scores_list: NDArray[np.float32],
|
||||||
|
nms_threshold: float = 0.4,
|
||||||
|
) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
||||||
|
"""
|
||||||
|
Filter postprocessed network outputs with NMS
|
||||||
|
|
||||||
|
:param bboxes_list: List of bboxes (np.ndarray)
|
||||||
|
:param kpss_list: List of keypoints (np.ndarray)
|
||||||
|
:param scores_list: List of scores (np.ndarray)
|
||||||
|
:return: Face bboxes with scores [t,l,b,r,score], and key points
|
||||||
|
"""
|
||||||
|
|
||||||
|
pre_det = np.hstack((bboxes_list, scores_list))
|
||||||
|
keep = nms(pre_det, threshold=nms_threshold)
|
||||||
|
det = pre_det[keep, :]
|
||||||
|
kpss = kpss_list[keep, :]
|
||||||
|
kpss = kpss.reshape((kpss.shape[0], -1, 2))
|
||||||
|
|
||||||
|
return det, kpss
|
||||||
|
|
||||||
|
|
||||||
|
class SCRFD:
|
||||||
|
def __init__(self, session: ModelSession):
|
||||||
|
self.session = session
|
||||||
|
self.center_cache: dict[tuple[int, int], NDArray[np.float32]] = {}
|
||||||
|
self.nms_threshold = 0.4
|
||||||
|
self.fmc = 3
|
||||||
|
self._feat_stride_fpn = [8, 16, 32]
|
||||||
|
self._num_anchors = 2
|
||||||
|
|
||||||
|
def prepare(self, nms_threshold: float = 0.4) -> None:
|
||||||
|
"""
|
||||||
|
Populate class parameters
|
||||||
|
|
||||||
|
:param nms_threshold: Threshold for NMS IoU
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.nms_threshold = nms_threshold
|
||||||
|
|
||||||
|
def detect(
|
||||||
|
self, imgs: NDArray[np.uint8], threshold: float = 0.5
|
||||||
|
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]]]:
|
||||||
|
"""
|
||||||
|
Run detection pipeline for provided images
|
||||||
|
|
||||||
|
:param img: Raw image as nd.ndarray with HWC shape
|
||||||
|
:param threshold: Confidence threshold
|
||||||
|
:return: Face bboxes with scores [t,l,b,r,score], and key points
|
||||||
|
"""
|
||||||
|
|
||||||
|
height, width = imgs.shape[1:3]
|
||||||
|
blob = self._preprocess(imgs)
|
||||||
|
net_outs = self._forward(blob)
|
||||||
|
|
||||||
|
batch_bboxes, batch_kpss, batch_scores = self._postprocess(net_outs, height, width, threshold)
|
||||||
|
|
||||||
|
dets_list = []
|
||||||
|
kpss_list = []
|
||||||
|
for e in range(imgs.shape[0]):
|
||||||
|
if len(batch_bboxes[e]) == 0:
|
||||||
|
det, kpss = np.zeros((0, 5), dtype="float32"), np.zeros((0, 10), dtype="float32")
|
||||||
|
else:
|
||||||
|
det, kpss = filter(batch_bboxes[e], batch_kpss[e], batch_scores[e], self.nms_threshold)
|
||||||
|
|
||||||
|
dets_list.append(det)
|
||||||
|
kpss_list.append(kpss)
|
||||||
|
|
||||||
|
return dets_list, kpss_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_anchors(
|
||||||
|
input_height: int, input_width: int, strides: list[int], num_anchors: int
|
||||||
|
) -> NDArray[np.float32]:
|
||||||
|
"""
|
||||||
|
Precompute anchor points for provided image size
|
||||||
|
|
||||||
|
:param input_height: Input image height
|
||||||
|
:param input_width: Input image width
|
||||||
|
:param strides: Model strides
|
||||||
|
:param num_anchors: Model num anchors
|
||||||
|
:return: box centers
|
||||||
|
"""
|
||||||
|
|
||||||
|
centers = []
|
||||||
|
for stride in strides:
|
||||||
|
height = input_height // stride
|
||||||
|
width = input_width // stride
|
||||||
|
|
||||||
|
anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
|
||||||
|
anchor_centers = (anchor_centers * stride).reshape((-1, 2))
|
||||||
|
if num_anchors > 1:
|
||||||
|
anchor_centers = np.stack([anchor_centers] * num_anchors, axis=1).reshape((-1, 2))
|
||||||
|
centers.append(anchor_centers)
|
||||||
|
return centers
|
||||||
|
|
||||||
|
def _preprocess(self, images: NDArray[np.uint8]):
|
||||||
|
"""
|
||||||
|
Normalize image on CPU if backend can't provide CUDA stream,
|
||||||
|
otherwise preprocess image on GPU using CuPy
|
||||||
|
|
||||||
|
:param img: Raw image as np.ndarray with HWC shape
|
||||||
|
:return: Preprocessed image or None if image was processed on device
|
||||||
|
"""
|
||||||
|
|
||||||
|
input_size = tuple(images[0].shape[0:2][::-1])
|
||||||
|
return cv2.dnn.blobFromImages(images, 1.0 / 128, input_size, (127.5, 127.5, 127.5), swapRB=False)
|
||||||
|
|
||||||
|
def _forward(self, blob: NDArray[np.float32]) -> list[NDArray[np.float32]]:
|
||||||
|
"""
|
||||||
|
Send input data to inference backend.
|
||||||
|
|
||||||
|
:param blob: Preprocessed image of shape NCHW or None
|
||||||
|
:return: network outputs
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.session.run(None, {"input.1": blob})
|
||||||
|
|
||||||
|
def _postprocess(
|
||||||
|
self, net_outs: list[NDArray[np.float32]], height: int, width: int, threshold: float
|
||||||
|
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]], list[NDArray[np.float32]]]:
|
||||||
|
"""
|
||||||
|
Precompute anchor points for provided image size and process network outputs
|
||||||
|
|
||||||
|
:param net_outs: Network outputs
|
||||||
|
:param input_height: Input image height
|
||||||
|
:param input_width: Input image width
|
||||||
|
:param threshold: Confidence threshold
|
||||||
|
:return: filtered bboxes, keypoints and scores
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = (height, width)
|
||||||
|
|
||||||
|
if not self.center_cache.get(key):
|
||||||
|
self.center_cache[key] = self._build_anchors(height, width, self._feat_stride_fpn, self._num_anchors)
|
||||||
|
anchor_centers = self.center_cache[key]
|
||||||
|
bboxes, kpss, scores = self._process_strides(net_outs, threshold, anchor_centers)
|
||||||
|
return bboxes, kpss, scores
|
||||||
|
|
||||||
|
def _process_strides(
|
||||||
|
self, net_outs: list[NDArray[np.float32]], threshold: float, anchors: NDArray[np.float32]
|
||||||
|
) -> tuple[list[NDArray[np.float32]], list[NDArray[np.float32]], list[NDArray[np.float32]]]:
|
||||||
|
"""
|
||||||
|
Process network outputs by strides and return results proposals filtered by threshold
|
||||||
|
|
||||||
|
:param net_outs: Network outputs
|
||||||
|
:param threshold: Confidence threshold
|
||||||
|
:param anchor_centers: Precomputed anchor centers for all strides
|
||||||
|
:return: filtered bboxes, keypoints and scores
|
||||||
|
"""
|
||||||
|
|
||||||
|
batch_size = net_outs[0].shape[0]
|
||||||
|
bboxes_by_img = []
|
||||||
|
kpss_by_img = []
|
||||||
|
scores_by_img = []
|
||||||
|
|
||||||
|
for batch in range(batch_size):
|
||||||
|
scores_strided = []
|
||||||
|
bboxes_strided = []
|
||||||
|
kpss_strided = []
|
||||||
|
for idx, stride in enumerate(self._feat_stride_fpn):
|
||||||
|
score_blob = net_outs[idx][batch]
|
||||||
|
bbox_blob = net_outs[idx + self.fmc][batch]
|
||||||
|
kpss_blob = net_outs[idx + self.fmc * 2][batch]
|
||||||
|
stride_anchors = anchors[idx]
|
||||||
|
score_list, bbox_list, kpss_list = generate_proposals(
|
||||||
|
score_blob,
|
||||||
|
bbox_blob,
|
||||||
|
kpss_blob,
|
||||||
|
stride,
|
||||||
|
stride_anchors,
|
||||||
|
threshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
scores_strided.append(score_list)
|
||||||
|
bboxes_strided.append(bbox_list)
|
||||||
|
kpss_strided.append(kpss_list)
|
||||||
|
bboxes_by_img.append(np.concatenate(bboxes_strided, axis=0))
|
||||||
|
kpss_by_img.append(np.concatenate(kpss_strided, axis=0))
|
||||||
|
scores_by_img.append(np.concatenate(scores_strided, axis=0))
|
||||||
|
|
||||||
|
return bboxes_by_img, kpss_by_img, scores_by_img
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import onnx
|
||||||
|
import onnxruntime as ort
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
from onnx.shape_inference import infer_shapes
|
||||||
|
from onnx.tools.update_model_dims import update_inputs_outputs_dims
|
||||||
|
|
||||||
|
|
||||||
|
def ort_has_batch_dim(session: ort.InferenceSession) -> bool:
|
||||||
|
return session.get_inputs()[0].shape[0] == "batch"
|
||||||
|
|
||||||
|
|
||||||
|
def ort_expand_outputs(session: ort.InferenceSession) -> None:
|
||||||
|
original_run = session.run
|
||||||
|
|
||||||
|
def run(output_names: list[str], input_feed: dict[str, NDArray[np.float32]]) -> list[NDArray[np.float32]]:
|
||||||
|
out: list[NDArray[np.float32]] = original_run(output_names, input_feed)
|
||||||
|
out = [np.expand_dims(o, axis=0) for o in out]
|
||||||
|
return out
|
||||||
|
|
||||||
|
session.run = run
|
||||||
|
|
||||||
|
|
||||||
|
def ort_add_batch_dim(input_path: Path, output_path: Path) -> None:
|
||||||
|
proto = onnx.load(input_path)
|
||||||
|
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
|
||||||
|
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]
|
||||||
|
input_dims = {proto.graph.input[0].name: ["batch"] + static_input_dims}
|
||||||
|
output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
|
||||||
|
updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
|
||||||
|
inferred = infer_shapes(updated_proto)
|
||||||
|
onnx.save(inferred, output_path)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import IO
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from numba import njit
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -30,10 +31,11 @@ def to_numpy(img: Image.Image) -> NDArray[np.float32]:
|
|||||||
return np.asarray(img if img.mode == "RGB" else img.convert("RGB"), dtype=np.float32) / 255.0
|
return np.asarray(img if img.mode == "RGB" else img.convert("RGB"), dtype=np.float32) / 255.0
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True, fastmath=True, nogil=True)
|
||||||
def normalize(
|
def normalize(
|
||||||
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
|
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
|
||||||
) -> NDArray[np.float32]:
|
) -> NDArray[np.float32]:
|
||||||
return np.divide(img - mean, std, dtype=np.float32)
|
return (img - mean) / std
|
||||||
|
|
||||||
|
|
||||||
def get_pil_resampling(resample: str) -> Image.Resampling:
|
def get_pil_resampling(resample: str) -> Image.Resampling:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from types import SimpleNamespace
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from app.models.session import ort_add_batch_dim, ort_has_batch_dim, ort_squeeze_outputs
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
@@ -734,3 +735,69 @@ class TestEndpoints:
|
|||||||
assert expected_face["boundingBox"] == actual_face["boundingBox"]
|
assert expected_face["boundingBox"] == actual_face["boundingBox"]
|
||||||
assert np.allclose(expected_face["embedding"], actual_face["embedding"])
|
assert np.allclose(expected_face["embedding"], actual_face["embedding"])
|
||||||
assert np.allclose(expected_face["score"], actual_face["score"])
|
assert np.allclose(expected_face["score"], actual_face["score"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionUtils:
|
||||||
|
def test_ort_has_batch_dim(self, mocker: MockerFixture) -> None:
|
||||||
|
mock_session = mocker.Mock(spec=ort.InferenceSession)
|
||||||
|
mock_session.get_inputs.return_value = [SimpleNamespace(shape=["batch", 3, 224, 224], name="input.1")]
|
||||||
|
|
||||||
|
assert ort_has_batch_dim(mock_session) is True
|
||||||
|
|
||||||
|
def test_ort_has_no_batch_dim(self, mocker: MockerFixture) -> None:
|
||||||
|
mock_session = mocker.Mock(spec=ort.InferenceSession)
|
||||||
|
mock_session.get_inputs.return_value = [SimpleNamespace(shape=[1, 3, 224, 224], name="input.1")]
|
||||||
|
|
||||||
|
assert ort_has_batch_dim(mock_session) is False
|
||||||
|
|
||||||
|
def test_ort_squeeze_outputs(self, mocker: MockerFixture) -> None:
|
||||||
|
mock_session = mocker.Mock(spec=ort.InferenceSession)
|
||||||
|
mock_session.run.return_value = [np.random.rand(1, 3, 224, 224).astype(np.float32)]
|
||||||
|
|
||||||
|
ort_squeeze_outputs(mock_session)
|
||||||
|
out = mock_session.run(["output"], {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)})
|
||||||
|
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0].shape == (3, 224, 224)
|
||||||
|
|
||||||
|
def test_ort_add_batch_dim(self, mocker: MockerFixture) -> None:
|
||||||
|
mock_proto = mocker.Mock()
|
||||||
|
mock_proto.graph.input = [
|
||||||
|
SimpleNamespace(name="input", type=mock.Mock(tensor_type=SimpleNamespace(shape=SimpleNamespace())))
|
||||||
|
]
|
||||||
|
mock_proto.graph.output = [
|
||||||
|
SimpleNamespace(name="output", type=SimpleNamespace(tensor_type=SimpleNamespace(shape=SimpleNamespace())))
|
||||||
|
]
|
||||||
|
mock_proto.graph.input[0].type.tensor_type.shape.dim = [
|
||||||
|
SimpleNamespace(dim_value=3),
|
||||||
|
SimpleNamespace(dim_value=224),
|
||||||
|
SimpleNamespace(dim_value=224),
|
||||||
|
]
|
||||||
|
mock_proto.graph.output[0].type.tensor_type.shape.dim = [
|
||||||
|
SimpleNamespace(dim_value=3),
|
||||||
|
SimpleNamespace(dim_value=224),
|
||||||
|
SimpleNamespace(dim_value=224),
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_load = mocker.patch("app.models.session.onnx.load")
|
||||||
|
mock_load.return_value = mock_proto
|
||||||
|
|
||||||
|
mock_update_dims = mocker.patch("app.models.session.update_inputs_outputs_dims")
|
||||||
|
mock_updated = mocker.Mock()
|
||||||
|
mock_update_dims.return_value = mock_updated
|
||||||
|
|
||||||
|
mock_infer_shapes = mocker.patch("app.models.session.infer_shapes")
|
||||||
|
mock_inferred = mocker.Mock()
|
||||||
|
mock_infer_shapes.return_value = mock_inferred
|
||||||
|
|
||||||
|
mock_save = mocker.patch("app.models.session.onnx.save")
|
||||||
|
|
||||||
|
input_path, output_path = Path("input.onnx"), Path("output.onnx")
|
||||||
|
ort_add_batch_dim(input_path, output_path)
|
||||||
|
|
||||||
|
mock_load.assert_called_once_with(input_path)
|
||||||
|
mock_save.assert_called_once_with(mock_inferred, output_path)
|
||||||
|
mock_update_dims.assert_called_once_with(mock_proto, mock.ANY, mock.ANY)
|
||||||
|
assert mock_update_dims.call_args_list == [
|
||||||
|
mock.call(mock_proto, {"input": ["batch", 224, 224]}, {"output": ["batch", 224, 224]})
|
||||||
|
]
|
||||||
|
|||||||
Generated
+65
-5
@@ -1528,6 +1528,36 @@ files = [
|
|||||||
lint = ["pre-commit (>=3.3)"]
|
lint = ["pre-commit (>=3.3)"]
|
||||||
test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "llvmlite"
|
||||||
|
version = "0.42.0"
|
||||||
|
description = "lightweight wrapper around basic LLVM functionality"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "llvmlite-0.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3366938e1bf63d26c34fbfb4c8e8d2ded57d11e0567d5bb243d89aab1eb56098"},
|
||||||
|
{file = "llvmlite-0.42.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c35da49666a21185d21b551fc3caf46a935d54d66969d32d72af109b5e7d2b6f"},
|
||||||
|
{file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f44ccc3c6220bd23e0ba698a63ec2a7d3205da0d848804807f37fc243e3f77"},
|
||||||
|
{file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f8d8717a9073b9e0246998de89929071d15b47f254c10eef2310b9aac033d"},
|
||||||
|
{file = "llvmlite-0.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d90edf400b4ceb3a0e776b6c6e4656d05c7187c439587e06f86afceb66d2be5"},
|
||||||
|
{file = "llvmlite-0.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae511caed28beaf1252dbaf5f40e663f533b79ceb408c874c01754cafabb9cbf"},
|
||||||
|
{file = "llvmlite-0.42.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81e674c2fe85576e6c4474e8c7e7aba7901ac0196e864fe7985492b737dbab65"},
|
||||||
|
{file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3975787f13eb97629052edb5017f6c170eebc1c14a0433e8089e5db43bcce6"},
|
||||||
|
{file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5bece0cdf77f22379f19b1959ccd7aee518afa4afbd3656c6365865f84903f9"},
|
||||||
|
{file = "llvmlite-0.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e0c4c11c8c2aa9b0701f91b799cb9134a6a6de51444eff5a9087fc7c1384275"},
|
||||||
|
{file = "llvmlite-0.42.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:08fa9ab02b0d0179c688a4216b8939138266519aaa0aa94f1195a8542faedb56"},
|
||||||
|
{file = "llvmlite-0.42.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2fce7d355068494d1e42202c7aff25d50c462584233013eb4470c33b995e3ee"},
|
||||||
|
{file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe66a86dc44634b59a3bc860c7b20d26d9aaffcd30364ebe8ba79161a9121f4"},
|
||||||
|
{file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47494552559e00d81bfb836cf1c4d5a5062e54102cc5767d5aa1e77ccd2505c"},
|
||||||
|
{file = "llvmlite-0.42.0-cp312-cp312-win_amd64.whl", hash = "sha256:05cb7e9b6ce69165ce4d1b994fbdedca0c62492e537b0cc86141b6e2c78d5888"},
|
||||||
|
{file = "llvmlite-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bdd3888544538a94d7ec99e7c62a0cdd8833609c85f0c23fcb6c5c591aec60ad"},
|
||||||
|
{file = "llvmlite-0.42.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0936c2067a67fb8816c908d5457d63eba3e2b17e515c5fe00e5ee2bace06040"},
|
||||||
|
{file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a78ab89f1924fc11482209f6799a7a3fc74ddc80425a7a3e0e8174af0e9e2301"},
|
||||||
|
{file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7599b65c7af7abbc978dbf345712c60fd596aa5670496561cc10e8a71cebfb2"},
|
||||||
|
{file = "llvmlite-0.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:43d65cc4e206c2e902c1004dd5418417c4efa6c1d04df05c6c5675a27e8ca90e"},
|
||||||
|
{file = "llvmlite-0.42.0.tar.gz", hash = "sha256:f92b09243c0cc3f457da8b983f67bd8e1295d0f5b3746c7a1861d7a99403854a"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locust"
|
name = "locust"
|
||||||
version = "2.28.0"
|
version = "2.28.0"
|
||||||
@@ -1864,6 +1894,40 @@ doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.
|
|||||||
extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"]
|
extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"]
|
||||||
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
|
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numba"
|
||||||
|
version = "0.59.1"
|
||||||
|
description = "compiling Python code using LLVM"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "numba-0.59.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97385a7f12212c4f4bc28f648720a92514bee79d7063e40ef66c2d30600fd18e"},
|
||||||
|
{file = "numba-0.59.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b77aecf52040de2a1eb1d7e314497b9e56fba17466c80b457b971a25bb1576d"},
|
||||||
|
{file = "numba-0.59.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3476a4f641bfd58f35ead42f4dcaf5f132569c4647c6f1360ccf18ee4cda3990"},
|
||||||
|
{file = "numba-0.59.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:525ef3f820931bdae95ee5379c670d5c97289c6520726bc6937a4a7d4230ba24"},
|
||||||
|
{file = "numba-0.59.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e395e44d192a12105eca3083b61307db7da10e093972ca285c85bef0963d6"},
|
||||||
|
{file = "numba-0.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43727e7ad20b3ec23ee4fc642f5b61845c71f75dd2825b3c234390c6d8d64051"},
|
||||||
|
{file = "numba-0.59.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:411df625372c77959570050e861981e9d196cc1da9aa62c3d6a836b5cc338966"},
|
||||||
|
{file = "numba-0.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2801003caa263d1e8497fb84829a7ecfb61738a95f62bc05693fcf1733e978e4"},
|
||||||
|
{file = "numba-0.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd2842fac03be4e5324ebbbd4d2d0c8c0fc6e0df75c09477dd45b288a0777389"},
|
||||||
|
{file = "numba-0.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:0594b3dfb369fada1f8bb2e3045cd6c61a564c62e50cf1f86b4666bc721b3450"},
|
||||||
|
{file = "numba-0.59.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1cce206a3b92836cdf26ef39d3a3242fec25e07f020cc4feec4c4a865e340569"},
|
||||||
|
{file = "numba-0.59.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c8b4477763cb1fbd86a3be7050500229417bf60867c93e131fd2626edb02238"},
|
||||||
|
{file = "numba-0.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d80bce4ef7e65bf895c29e3889ca75a29ee01da80266a01d34815918e365835"},
|
||||||
|
{file = "numba-0.59.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7ad1d217773e89a9845886401eaaab0a156a90aa2f179fdc125261fd1105096"},
|
||||||
|
{file = "numba-0.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bf68f4d69dd3a9f26a9b23548fa23e3bcb9042e2935257b471d2a8d3c424b7f"},
|
||||||
|
{file = "numba-0.59.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e0318ae729de6e5dbe64c75ead1a95eb01fabfe0e2ebed81ebf0344d32db0ae"},
|
||||||
|
{file = "numba-0.59.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f68589740a8c38bb7dc1b938b55d1145244c8353078eea23895d4f82c8b9ec1"},
|
||||||
|
{file = "numba-0.59.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:649913a3758891c77c32e2d2a3bcbedf4a69f5fea276d11f9119677c45a422e8"},
|
||||||
|
{file = "numba-0.59.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9712808e4545270291d76b9a264839ac878c5eb7d8b6e02c970dc0ac29bc8187"},
|
||||||
|
{file = "numba-0.59.1-cp39-cp39-win_amd64.whl", hash = "sha256:8d51ccd7008a83105ad6a0082b6a2b70f1142dc7cfd76deb8c5a862367eb8c86"},
|
||||||
|
{file = "numba-0.59.1.tar.gz", hash = "sha256:76f69132b96028d2774ed20415e8c528a34e3299a40581bae178f0994a2f370b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
llvmlite = "==0.42.*"
|
||||||
|
numpy = ">=1.22,<1.27"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "1.26.3"
|
version = "1.26.3"
|
||||||
@@ -2037,11 +2101,8 @@ description = "ONNX Runtime is a runtime accelerator for Machine Learning models
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed693011b472f9a617b2d5c4785d5fa1e1b77f7cb2b02e47b899534ec6c6396"},
|
|
||||||
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:5152b5e56e83e022ced2986700d68dd8ba7b1466761725ce774f679c5710ab87"},
|
{file = "onnxruntime_openvino-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:5152b5e56e83e022ced2986700d68dd8ba7b1466761725ce774f679c5710ab87"},
|
||||||
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce3b1aa06d6b8b732d314d217028ec4735de5806215c44d3bdbcad03b9260d5"},
|
|
||||||
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:21133a701bb07ea19e01f48b8c23beee575f2e879f49173843f275d7c91a625a"},
|
{file = "onnxruntime_openvino-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:21133a701bb07ea19e01f48b8c23beee575f2e879f49173843f275d7c91a625a"},
|
||||||
{file = "onnxruntime_openvino-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76824dac3c392ad4b812f29c18be2055ab3bba2e3c111e44baae847b33d5b081"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2601,7 +2662,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||||
@@ -3572,4 +3632,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.12"
|
python-versions = ">=3.10,<3.12"
|
||||||
content-hash = "db51ad1e631b569e106927683a13124252bd80974def1f2edbe23ac87d89c461"
|
content-hash = "a44e079d565fc1166458690ca2dc5826e198cc07ccab0ebaf71b5ab5e0eed150"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.106.0"
|
version = "1.105.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -23,6 +23,7 @@ orjson = ">=3.9.5"
|
|||||||
gunicorn = ">=21.1.0"
|
gunicorn = ">=21.1.0"
|
||||||
huggingface-hub = ">=0.20.1,<1.0"
|
huggingface-hub = ">=0.20.1,<1.0"
|
||||||
tokenizers = ">=0.15.0,<1.0"
|
tokenizers = ">=0.15.0,<1.0"
|
||||||
|
numba = "^0.59.1"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
mypy = ">=1.3.0"
|
mypy = ">=1.3.0"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 141,
|
"android.injected.version.code" => 140,
|
||||||
"android.injected.version.name" => "1.106.0",
|
"android.injected.version.name" => "1.105.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.106.0"
|
version_number: "1.105.1"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/forms/login/login_form.dart';
|
import 'package:immich_mobile/widgets/forms/login_form.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class AssetService {
|
|||||||
const int chunkSize = 10000;
|
const int chunkSize = 10000;
|
||||||
try {
|
try {
|
||||||
final List<Asset> allAssets = [];
|
final List<Asset> allAssets = [];
|
||||||
|
DateTime? lastCreationDate;
|
||||||
String? lastId;
|
String? lastId;
|
||||||
// will break on error or once all assets are loaded
|
// will break on error or once all assets are loaded
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -108,17 +109,15 @@ class AssetService {
|
|||||||
limit: chunkSize,
|
limit: chunkSize,
|
||||||
updatedUntil: until,
|
updatedUntil: until,
|
||||||
lastId: lastId,
|
lastId: lastId,
|
||||||
|
lastCreationDate: lastCreationDate,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
);
|
);
|
||||||
log.fine("Requesting $chunkSize assets from $lastId");
|
|
||||||
final List<AssetResponseDto>? assets =
|
final List<AssetResponseDto>? assets =
|
||||||
await _apiService.syncApi.getFullSyncForUser(dto);
|
await _apiService.syncApi.getFullSyncForUser(dto);
|
||||||
if (assets == null) return null;
|
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));
|
allAssets.addAll(assets.map(Asset.remote));
|
||||||
if (assets.length != chunkSize) break;
|
if (assets.isEmpty) break;
|
||||||
|
lastCreationDate = assets.last.fileCreatedAt;
|
||||||
lastId = assets.last.id;
|
lastId = assets.last.id;
|
||||||
}
|
}
|
||||||
return allAssets;
|
return allAssets;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+216
-63
@@ -15,19 +15,11 @@ import 'package:immich_mobile/providers/asset.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.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_logo.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.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:openapi/api.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class LoginForm extends HookConsumerWidget {
|
class LoginForm extends HookConsumerWidget {
|
||||||
@@ -53,35 +45,9 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
final logoAnimationController = useAnimationController(
|
final logoAnimationController = useAnimationController(
|
||||||
duration: const Duration(seconds: 60),
|
duration: const Duration(seconds: 60),
|
||||||
)..repeat();
|
)..repeat();
|
||||||
final serverInfo = ref.watch(serverInfoProvider);
|
|
||||||
final warningMessage = useState<String>('');
|
|
||||||
|
|
||||||
final ValueNotifier<String?> serverEndpoint = useState<String?>(null);
|
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
|
/// Fetch the server login credential and enables oAuth login if necessary
|
||||||
/// Returns true if successful, false otherwise
|
/// Returns true if successful, false otherwise
|
||||||
Future<bool> getServerLoginCredential() async {
|
Future<bool> getServerLoginCredential() async {
|
||||||
@@ -342,40 +308,11 @@ 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() {
|
buildLogin() {
|
||||||
return AutofillGroup(
|
return AutofillGroup(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
buildVersionCompatWarning(),
|
|
||||||
Text(
|
Text(
|
||||||
sanitizeUrl(serverEndpointController.text),
|
sanitizeUrl(serverEndpointController.text),
|
||||||
style: context.textTheme.displaySmall,
|
style: context.textTheme.displaySmall,
|
||||||
@@ -479,6 +416,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
// Note: This used to have an AnimatedSwitcher, but was removed
|
// Note: This used to have an AnimatedSwitcher, but was removed
|
||||||
// because of https://github.com/flutter/flutter/issues/120874
|
// because of https://github.com/flutter/flutter/issues/120874
|
||||||
@@ -492,3 +430,218 @@ 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.106.0
|
- API version: 1.105.1
|
||||||
- Generator version: 7.5.0
|
- Generator version: 7.5.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -13,12 +13,21 @@ part of openapi.api;
|
|||||||
class AssetFullSyncDto {
|
class AssetFullSyncDto {
|
||||||
/// Returns a new [AssetFullSyncDto] instance.
|
/// Returns a new [AssetFullSyncDto] instance.
|
||||||
AssetFullSyncDto({
|
AssetFullSyncDto({
|
||||||
|
this.lastCreationDate,
|
||||||
this.lastId,
|
this.lastId,
|
||||||
required this.limit,
|
required this.limit,
|
||||||
required this.updatedUntil,
|
required this.updatedUntil,
|
||||||
this.userId,
|
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
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@@ -42,6 +51,7 @@ class AssetFullSyncDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto &&
|
||||||
|
other.lastCreationDate == lastCreationDate &&
|
||||||
other.lastId == lastId &&
|
other.lastId == lastId &&
|
||||||
other.limit == limit &&
|
other.limit == limit &&
|
||||||
other.updatedUntil == updatedUntil &&
|
other.updatedUntil == updatedUntil &&
|
||||||
@@ -50,16 +60,22 @@ class AssetFullSyncDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(lastCreationDate == null ? 0 : lastCreationDate!.hashCode) +
|
||||||
(lastId == null ? 0 : lastId!.hashCode) +
|
(lastId == null ? 0 : lastId!.hashCode) +
|
||||||
(limit.hashCode) +
|
(limit.hashCode) +
|
||||||
(updatedUntil.hashCode) +
|
(updatedUntil.hashCode) +
|
||||||
(userId == null ? 0 : userId!.hashCode);
|
(userId == null ? 0 : userId!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
|
String toString() => 'AssetFullSyncDto[lastCreationDate=$lastCreationDate, lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.lastCreationDate != null) {
|
||||||
|
json[r'lastCreationDate'] = this.lastCreationDate!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'lastCreationDate'] = null;
|
||||||
|
}
|
||||||
if (this.lastId != null) {
|
if (this.lastId != null) {
|
||||||
json[r'lastId'] = this.lastId;
|
json[r'lastId'] = this.lastId;
|
||||||
} else {
|
} else {
|
||||||
@@ -83,6 +99,7 @@ class AssetFullSyncDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AssetFullSyncDto(
|
return AssetFullSyncDto(
|
||||||
|
lastCreationDate: mapDateTime(json, r'lastCreationDate', r''),
|
||||||
lastId: mapValueOfType<String>(json, r'lastId'),
|
lastId: mapValueOfType<String>(json, r'lastId'),
|
||||||
limit: mapValueOfType<int>(json, r'limit')!,
|
limit: mapValueOfType<int>(json, r'limit')!,
|
||||||
updatedUntil: mapDateTime(json, r'updatedUntil', r'')!,
|
updatedUntil: mapDateTime(json, r'updatedUntil', r'')!,
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.106.0+141
|
version: 1.105.1+140
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -7432,6 +7432,10 @@
|
|||||||
},
|
},
|
||||||
"AssetFullSyncDto": {
|
"AssetFullSyncDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"lastCreationDate": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"lastId": {
|
"lastId": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.106.0
|
* 1.105.1
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@@ -904,6 +904,7 @@ export type AssetDeltaSyncResponseDto = {
|
|||||||
upserted: AssetResponseDto[];
|
upserted: AssetResponseDto[];
|
||||||
};
|
};
|
||||||
export type AssetFullSyncDto = {
|
export type AssetFullSyncDto = {
|
||||||
|
lastCreationDate?: string;
|
||||||
lastId?: string;
|
lastId?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
updatedUntil: string;
|
updatedUntil: string;
|
||||||
|
|||||||
Generated
+21
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
|
"@types/imagemin": "^8.0.1",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
@@ -5789,6 +5790,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
"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": {
|
"node_modules/@types/inquirer": {
|
||||||
"version": "8.2.6",
|
"version": "8.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
|
||||||
@@ -20204,6 +20214,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
"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": {
|
"@types/inquirer": {
|
||||||
"version": "8.2.6",
|
"version": "8.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
|
"@types/imagemin": "^8.0.1",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
|
|||||||
@@ -374,8 +374,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
|
DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
|
||||||
|
|
||||||
IMMICH_PORT: Joi.number().optional(),
|
IMMICH_PORT: Joi.number().optional(),
|
||||||
IMMICH_API_METRICS_PORT: Joi.number().optional(),
|
IMMICH_METRICS_PORT: Joi.number().optional(),
|
||||||
IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(),
|
|
||||||
|
|
||||||
IMMICH_METRICS: Joi.boolean().optional().default(false),
|
IMMICH_METRICS: Joi.boolean().optional().default(false),
|
||||||
IMMICH_HOST_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,
|
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
|
||||||
stack: withStack
|
stack: withStack
|
||||||
? entity.stack?.assets
|
? entity.stack?.assets
|
||||||
?.filter((a) => a.id !== entity.stack?.primaryAssetId)
|
.filter((a) => a.id !== entity.stack?.primaryAssetId)
|
||||||
?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
|
.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
|
||||||
: undefined,
|
: undefined,
|
||||||
stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null,
|
stackCount: entity.stack?.assets?.length ?? null,
|
||||||
isOffline: entity.isOffline,
|
isOffline: entity.isOffline,
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
duplicateId: entity.duplicateId,
|
duplicateId: entity.duplicateId,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export class AssetFullSyncDto {
|
|||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
lastId?: string;
|
lastId?: string;
|
||||||
|
|
||||||
|
@ValidateDate({ optional: true })
|
||||||
|
lastCreationDate?: Date;
|
||||||
|
|
||||||
@ValidateDate()
|
@ValidateDate()
|
||||||
updatedUntil!: Date;
|
updatedUntil!: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,4 @@ export class AssetStackEntity {
|
|||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
primaryAssetId!: string;
|
primaryAssetId!: string;
|
||||||
|
|
||||||
assetCount?: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
|
|||||||
|
|
||||||
export interface AssetFullSyncOptions {
|
export interface AssetFullSyncOptions {
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
lastCreationDate?: Date;
|
||||||
lastId?: string;
|
lastId?: string;
|
||||||
updatedUntil: Date;
|
updatedUntil: Date;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|||||||
@@ -120,10 +120,6 @@ export interface IEntityJob extends IBaseJob {
|
|||||||
source?: 'upload' | 'sidecar-write' | 'copy';
|
source?: 'upload' | 'sidecar-write' | 'copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAssetDeleteJob extends IEntityJob {
|
|
||||||
deleteOnDisk: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILibraryFileJob extends IEntityJob {
|
export interface ILibraryFileJob extends IEntityJob {
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
assetPath: string;
|
assetPath: string;
|
||||||
@@ -250,7 +246,7 @@ export type JobItem =
|
|||||||
|
|
||||||
// Asset Deletion
|
// Asset Deletion
|
||||||
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
|
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
|
||||||
| { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
|
| { name: JobName.ASSET_DELETION; data: IEntityJob }
|
||||||
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
|
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
|
||||||
|
|
||||||
// Library Management
|
// Library Management
|
||||||
|
|||||||
@@ -1049,18 +1049,50 @@ SELECT
|
|||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."primaryAssetId" AS "stack_primaryAssetId"
|
"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"
|
||||||
FROM
|
FROM
|
||||||
"assets" "asset"
|
"assets" "asset"
|
||||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||||
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
|
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
|
WHERE
|
||||||
"asset"."isVisible" = true
|
"asset"."isVisible" = true
|
||||||
AND "asset"."ownerId" IN ($1)
|
AND "asset"."ownerId" IN ($1)
|
||||||
AND "asset"."id" > $2
|
AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3)
|
||||||
AND "asset"."updatedAt" <= $3
|
AND "asset"."updatedAt" <= $4
|
||||||
ORDER BY
|
ORDER BY
|
||||||
"asset"."id" ASC
|
"asset"."fileCreatedAt" DESC,
|
||||||
|
"asset"."id" DESC
|
||||||
LIMIT
|
LIMIT
|
||||||
10
|
10
|
||||||
|
|
||||||
@@ -1124,11 +1156,42 @@ SELECT
|
|||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."primaryAssetId" AS "stack_primaryAssetId"
|
"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"
|
||||||
FROM
|
FROM
|
||||||
"assets" "asset"
|
"assets" "asset"
|
||||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||||
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
|
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
|
WHERE
|
||||||
"asset"."isVisible" = true
|
"asset"."isVisible" = true
|
||||||
AND "asset"."ownerId" IN ($1)
|
AND "asset"."ownerId" IN ($1)
|
||||||
|
|||||||
@@ -763,40 +763,36 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
|
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
|
||||||
const { ownerId, lastId, updatedUntil, limit } = options;
|
const { ownerId, lastCreationDate, lastId, updatedUntil, limit } = options;
|
||||||
const builder = this.getBuilder({
|
const builder = this.getBuilder({
|
||||||
userIds: [ownerId],
|
userIds: [ownerId],
|
||||||
exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit`
|
exifInfo: true, // also joins stack information
|
||||||
withStacked: false, // return all assets individually as expected by the app
|
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 (lastId !== undefined) {
|
if (lastCreationDate !== undefined && lastId !== undefined) {
|
||||||
builder.andWhere('asset.id > :lastId', { lastId });
|
builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', {
|
||||||
|
lastCreationDate,
|
||||||
|
lastId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
builder
|
|
||||||
|
return builder
|
||||||
.andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil })
|
.andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil })
|
||||||
.orderBy('asset.id', 'ASC')
|
.orderBy('asset.fileCreatedAt', 'DESC')
|
||||||
.limit(limit) // cannot use `take` for performance reasons
|
.addOrderBy('asset.id', 'DESC')
|
||||||
.withDeleted();
|
.limit(limit)
|
||||||
return builder.getMany();
|
.withDeleted()
|
||||||
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] })
|
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] })
|
||||||
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
|
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
|
||||||
const builder = this.getBuilder({
|
const builder = this.getBuilder({ userIds: options.userIds, exifInfo: true, withStacked: false })
|
||||||
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) })
|
.andWhere({ updatedAt: MoreThan(options.updatedAfter) })
|
||||||
.limit(options.limit) // cannot use `take` for performance reasons
|
.limit(options.limit)
|
||||||
.withDeleted();
|
.withDeleted();
|
||||||
|
|
||||||
return builder.getMany();
|
return builder.getMany();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,8 +389,8 @@ describe(AssetService.name, () => {
|
|||||||
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
|
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
|
||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{ name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } },
|
{ name: JobName.ASSET_DELETION, data: { id: 'asset1' } },
|
||||||
{ name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } },
|
{ name: JobName.ASSET_DELETION, data: { id: 'asset2' } },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
assetMock.getById.mockResolvedValue(assetWithFace);
|
assetMock.getById.mockResolvedValue(assetWithFace);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetWithFace.id });
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
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 () => {
|
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
||||||
assetMock.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
assetMock.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
|
||||||
|
|
||||||
expect(assetStackMock.update).toHaveBeenCalledWith({
|
expect(assetStackMock.update).toHaveBeenCalledWith({
|
||||||
id: 'stack-1',
|
id: 'stack-1',
|
||||||
@@ -446,21 +446,10 @@ describe(AssetService.name, () => {
|
|||||||
it('should delete a live photo', async () => {
|
it('should delete a live photo', async () => {
|
||||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id });
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
|
||||||
deleteOnDisk: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
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,
|
name: JobName.DELETE_FILES,
|
||||||
@@ -474,7 +463,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should update usage', async () => {
|
it('should update usage', async () => {
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.image.id });
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
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 { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IAssetDeleteJob,
|
IEntityJob,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
ISidecarWriteJob,
|
ISidecarWriteJob,
|
||||||
JOBS_ASSET_PAGINATION_SIZE,
|
JOBS_ASSET_PAGINATION_SIZE,
|
||||||
@@ -256,21 +256,15 @@ export class AssetService {
|
|||||||
|
|
||||||
for await (const assets of assetPagination) {
|
for await (const assets of assetPagination) {
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
assets.map((asset) => ({
|
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
deleteOnDisk: true,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
|
async handleAssetDeletion(job: IEntityJob): Promise<JobStatus> {
|
||||||
const { id, deleteOnDisk } = job;
|
const { id } = job;
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
faces: {
|
faces: {
|
||||||
@@ -307,14 +301,12 @@ export class AssetService {
|
|||||||
|
|
||||||
// TODO refactor this to use cascades
|
// TODO refactor this to use cascades
|
||||||
if (asset.livePhotoVideoId) {
|
if (asset.livePhotoVideoId) {
|
||||||
await this.jobRepository.queue({
|
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: { id: asset.livePhotoVideoId, deleteOnDisk },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
|
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
|
||||||
if (deleteOnDisk) {
|
// skip originals if the user deleted the whole library
|
||||||
|
if (!asset.library?.deletedAt) {
|
||||||
files.push(asset.sidecarPath, asset.originalPath);
|
files.push(asset.sidecarPath, asset.originalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,12 +321,7 @@ export class AssetService {
|
|||||||
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
|
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } })));
|
||||||
ids.map((id) => ({
|
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: { id, deleteOnDisk: true },
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await this.assetRepository.softDeleteAll(ids);
|
await this.assetRepository.softDeleteAll(ids);
|
||||||
this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, 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);
|
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } },
|
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id } },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -355,13 +355,7 @@ export class LibraryService {
|
|||||||
const assetIds = await this.repository.getAssetIds(job.id, true);
|
const assetIds = await this.repository.getAssetIds(job.id, true);
|
||||||
this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`);
|
this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`);
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
assetIds.map((assetId) => ({
|
assetIds.map((assetId) => ({ name: JobName.ASSET_DELETION, data: { id: assetId } })),
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: {
|
|
||||||
id: assetId,
|
|
||||||
deleteOnDisk: false,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (assetIds.length === 0) {
|
if (assetIds.length === 0) {
|
||||||
@@ -550,13 +544,7 @@ export class LibraryService {
|
|||||||
for await (const assets of assetPagination) {
|
for await (const assets of assetPagination) {
|
||||||
this.logger.debug(`Removing ${assets.length} offline assets`);
|
this.logger.debug(`Removing ${assets.length} offline assets`);
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
assets.map((asset) => ({
|
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
deleteOnDisk: false,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ describe(MetadataService.name, () => {
|
|||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(jobMock.queue).toHaveBeenNthCalledWith(1, {
|
expect(jobMock.queue).toHaveBeenNthCalledWith(1, {
|
||||||
name: JobName.ASSET_DELETION,
|
name: JobName.ASSET_DELETION,
|
||||||
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true },
|
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId },
|
||||||
});
|
});
|
||||||
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
|
|||||||
@@ -460,10 +460,7 @@ export class MetadataService {
|
|||||||
// (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId)
|
// (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId)
|
||||||
// note asset.livePhotoVideoId is not motionAsset.id yet
|
// note asset.livePhotoVideoId is not motionAsset.id yet
|
||||||
if (asset.livePhotoVideoId) {
|
if (asset.livePhotoVideoId) {
|
||||||
await this.jobRepository.queue({
|
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: { id: asset.livePhotoVideoId, deleteOnDisk: true },
|
|
||||||
});
|
|
||||||
this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);
|
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 { SystemConfigService } from 'src/services/system-config.service';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { VersionService } from 'src/services/version.service';
|
import { VersionService } from 'src/services/version.service';
|
||||||
import { otelShutdown } from 'src/utils/instrumentation';
|
import { otelSDK } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicroservicesService {
|
export class MicroservicesService {
|
||||||
@@ -102,6 +102,6 @@ export class MicroservicesService {
|
|||||||
async teardown() {
|
async teardown() {
|
||||||
await this.libraryService.teardown();
|
await this.libraryService.teardown();
|
||||||
await this.metadataService.teardown();
|
await this.metadataService.teardown();
|
||||||
await otelShutdown();
|
await otelSDK.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class SyncService {
|
|||||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
||||||
const assets = await this.assetRepository.getAllForUserFullSync({
|
const assets = await this.assetRepository.getAllForUserFullSync({
|
||||||
ownerId: userId,
|
ownerId: userId,
|
||||||
|
lastCreationDate: dto.lastCreationDate,
|
||||||
updatedUntil: dto.updatedUntil,
|
updatedUntil: dto.updatedUntil,
|
||||||
lastId: dto.lastId,
|
lastId: dto.lastId,
|
||||||
limit: dto.limit,
|
limit: dto.limit,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe(TrashService.name, () => {
|
|||||||
assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
||||||
await expect(sut.empty(authStub.user1)).resolves.toBeUndefined();
|
await expect(sut.empty(authStub.user1)).resolves.toBeUndefined();
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
|
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id } },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,13 +49,7 @@ export class TrashService {
|
|||||||
|
|
||||||
for await (const assets of assetPagination) {
|
for await (const assets of assetPagination) {
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
assets.map((asset) => ({
|
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
|
||||||
name: JobName.ASSET_DELETION,
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
deleteOnDisk: true,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,18 +33,14 @@ const aggregation = new metrics.ExplicitBucketHistogramAggregation(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let otelSingleton: NodeSDK | undefined;
|
const metricsPort = Number.parseInt(process.env.IMMICH_METRICS_PORT ?? '8081');
|
||||||
|
|
||||||
export const otelStart = (port: number) => {
|
export const otelSDK = new NodeSDK({
|
||||||
if (otelSingleton) {
|
|
||||||
throw new Error('OpenTelemetry SDK already started');
|
|
||||||
}
|
|
||||||
otelSingleton = new NodeSDK({
|
|
||||||
resource: new resources.Resource({
|
resource: new resources.Resource({
|
||||||
[SemanticResourceAttributes.SERVICE_NAME]: `immich`,
|
[SemanticResourceAttributes.SERVICE_NAME]: `immich`,
|
||||||
[SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
|
[SemanticResourceAttributes.SERVICE_VERSION]: serverVersion.toString(),
|
||||||
}),
|
}),
|
||||||
metricReader: new PrometheusExporter({ port }),
|
metricReader: new PrometheusExporter({ port: metricsPort }),
|
||||||
contextManager: new AsyncLocalStorageContextManager(),
|
contextManager: new AsyncLocalStorageContextManager(),
|
||||||
instrumentations: [
|
instrumentations: [
|
||||||
new HttpInstrumentation(),
|
new HttpInstrumentation(),
|
||||||
@@ -54,15 +50,6 @@ export const otelStart = (port: number) => {
|
|||||||
],
|
],
|
||||||
views: [new metrics.View({ aggregation, instrumentName: '*', instrumentUnit: 'ms' })],
|
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 = {
|
export const otelConfig: OpenTelemetryModuleOptions = {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/const
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||||
import { ApiService } from 'src/services/api.service';
|
import { ApiService } from 'src/services/api.service';
|
||||||
import { otelStart } from 'src/utils/instrumentation';
|
import { otelSDK } from 'src/utils/instrumentation';
|
||||||
import { useSwagger } from 'src/utils/misc';
|
import { useSwagger } from 'src/utils/misc';
|
||||||
|
|
||||||
const host = process.env.HOST;
|
const host = process.env.HOST;
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
process.title = 'immich-api';
|
process.title = 'immich-api';
|
||||||
const otelPort = Number.parseInt(process.env.IMMICH_API_METRICS_PORT ?? '8081');
|
otelSDK.start();
|
||||||
|
|
||||||
otelStart(otelPort);
|
|
||||||
|
|
||||||
const port = Number(process.env.IMMICH_PORT) || 3001;
|
const port = Number(process.env.IMMICH_PORT) || 3001;
|
||||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { MicroservicesModule } from 'src/app.module';
|
|||||||
import { envName, serverVersion } from 'src/constants';
|
import { envName, serverVersion } from 'src/constants';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||||
import { otelStart } from 'src/utils/instrumentation';
|
import { otelSDK } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const otelPort = Number.parseInt(process.env.IMMICH_MICROSERVICES_METRICS_PORT ?? '8082');
|
otelSDK.start();
|
||||||
|
|
||||||
otelStart(otelPort);
|
|
||||||
|
|
||||||
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
|
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
|
||||||
const logger = await app.resolve(ILoggerRepository);
|
const logger = await app.resolve(ILoggerRepository);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ node_modules
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
*.md
|
*.md
|
||||||
|
*.json
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"organizeImportsSkipDestructiveCodeActions": true,
|
"singleQuote": true,
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
|
"trailingComma": "all",
|
||||||
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-svelte", "prettier-plugin-sort-json"],
|
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-svelte"],
|
||||||
"trailingComma": "all"
|
"organizeImportsSkipDestructiveCodeActions": true,
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3
-16
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
@@ -54,7 +54,6 @@
|
|||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"prettier-plugin-sort-json": "^4.0.0",
|
|
||||||
"prettier-plugin-svelte": "^3.2.1",
|
"prettier-plugin-svelte": "^3.2.1",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
@@ -7116,18 +7115,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/prettier-plugin-svelte": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.3.tgz",
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.0",
|
"version": "1.105.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
@@ -48,7 +48,6 @@
|
|||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"prettier-plugin-sort-json": "^4.0.0",
|
|
||||||
"prettier-plugin-svelte": "^3.2.1",
|
"prettier-plugin-svelte": "^3.2.1",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
@@ -75,8 +74,8 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-i18n": "^4.0.0",
|
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-maplibre": "^0.9.0",
|
"svelte-maplibre": "^0.9.0",
|
||||||
"thumbhash": "^0.1.1"
|
"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 CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js';
|
import { mdiDeleteOutline } from '@mdi/js';
|
||||||
import { type AssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{#if asset.isTrashed}
|
{#if asset.isTrashed}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="opaque"
|
color="opaque"
|
||||||
icon={mdiDeleteForeverOutline}
|
icon={mdiDeleteOutline}
|
||||||
on:click={() => dispatch('permanentlyDelete')}
|
on:click={() => dispatch('permanentlyDelete')}
|
||||||
title={$t('permanently_delete')}
|
title={$t('permanently_delete')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
|
on:wheel|preventDefault|nonpassive
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
||||||
{ shortcut: { key: 'c', meta: 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 MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { mdiTimerSand, mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js';
|
import { mdiTimerSand, mdiDeleteOutline } from '@mdi/js';
|
||||||
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
|
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
|
||||||
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
|
import DeleteAssetDialog from '../delete-asset-dialog.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
{:else if loading}
|
{:else if loading}
|
||||||
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title={label} icon={mdiDeleteForeverOutline} on:click={handleTrash} />
|
<CircleIconButton title={label} icon={mdiDeleteOutline} on:click={handleTrash} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowConfirmation}
|
{#if isShowConfirmation}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-51
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"account": "Konto",
|
"account": "Konto",
|
||||||
"acknowledge": "Bestätigen",
|
"acknowledge": "",
|
||||||
"action": "Aktion",
|
"action": "",
|
||||||
"actions": "Aktionen",
|
"actions": "",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"activity": "Aktivität",
|
"activity": "Aktivität",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
@@ -10,25 +10,25 @@
|
|||||||
"add_a_location": "Standort hinzufügen",
|
"add_a_location": "Standort hinzufügen",
|
||||||
"add_a_name": "Name hinzufügen",
|
"add_a_name": "Name hinzufügen",
|
||||||
"add_a_title": "Titel hinzufügen",
|
"add_a_title": "Titel hinzufügen",
|
||||||
"add_exclusion_pattern": "Ausschlussmuster hinzufügen",
|
"add_exclusion_pattern": "",
|
||||||
"add_import_path": "Importpfad hinzufügen",
|
"add_import_path": "",
|
||||||
"add_location": "Ort hinzufügen",
|
"add_location": "",
|
||||||
"add_more_users": "",
|
"add_more_users": "",
|
||||||
"add_partner": "Partner hinzufügen",
|
"add_partner": "",
|
||||||
"add_path": "Pfad hinzufügen",
|
"add_path": "",
|
||||||
"add_photos": "Fotos hinzufügen",
|
"add_photos": "",
|
||||||
"add_to": "Hinzufügen zu...",
|
"add_to": "Hinzufügen zu...",
|
||||||
"add_to_album": "Zu Album hinzufügen",
|
"add_to_album": "",
|
||||||
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
|
"add_to_shared_album": "",
|
||||||
"admin": {
|
"admin": {
|
||||||
"authentication_settings": "Authentifizierungseinstellungen",
|
"authentication_settings": "",
|
||||||
"authentication_settings_description": "Verwalten von Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen",
|
"authentication_settings_description": "",
|
||||||
"crontab_guru": "Crontab Guru",
|
"crontab_guru": "",
|
||||||
"disable_login": "Login deaktvieren",
|
"disable_login": "",
|
||||||
"disabled": "Deaktiviert",
|
"disabled": "Deaktiviert",
|
||||||
"duplicate_detection_job_description": "",
|
"duplicate_detection_job_description": "",
|
||||||
"image_format_description": "",
|
"image_format_description": "",
|
||||||
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
|
"image_prefer_embedded_preview": "",
|
||||||
"image_prefer_embedded_preview_setting_description": "",
|
"image_prefer_embedded_preview_setting_description": "",
|
||||||
"image_prefer_wide_gamut": "",
|
"image_prefer_wide_gamut": "",
|
||||||
"image_prefer_wide_gamut_setting_description": "",
|
"image_prefer_wide_gamut_setting_description": "",
|
||||||
@@ -218,27 +218,19 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
"albums": "",
|
||||||
"album_updated_setting_description": "",
|
"all": "",
|
||||||
"albums": "Alben",
|
"all_people": "",
|
||||||
"all": "Alle",
|
|
||||||
"all_people": "Alle Personen",
|
|
||||||
"allow_dark_mode": "",
|
"allow_dark_mode": "",
|
||||||
"allow_edits": "",
|
"allow_edits": "",
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
@@ -254,11 +246,11 @@
|
|||||||
"back": "",
|
"back": "",
|
||||||
"backward": "",
|
"backward": "",
|
||||||
"blurred_background": "",
|
"blurred_background": "",
|
||||||
"camera": "Kamera",
|
"camera": "",
|
||||||
"camera_brand": "",
|
"camera_brand": "",
|
||||||
"camera_model": "",
|
"camera_model": "",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "",
|
||||||
"cancel_search": "Suche abbrechen",
|
"cancel_search": "",
|
||||||
"cannot_merge_people": "",
|
"cannot_merge_people": "",
|
||||||
"cannot_update_the_description": "",
|
"cannot_update_the_description": "",
|
||||||
"cant_apply_changes": "",
|
"cant_apply_changes": "",
|
||||||
@@ -274,17 +266,17 @@
|
|||||||
"change_your_password": "",
|
"change_your_password": "",
|
||||||
"changed_visibility_successfully": "",
|
"changed_visibility_successfully": "",
|
||||||
"check_logs": "",
|
"check_logs": "",
|
||||||
"city": "Stadt",
|
"city": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"clear_all": "",
|
"clear_all": "",
|
||||||
"clear_message": "",
|
"clear_message": "",
|
||||||
"clear_value": "",
|
"clear_value": "",
|
||||||
"close": "Schliessen",
|
"close": "",
|
||||||
"collapse_all": "",
|
"collapse_all": "",
|
||||||
"color_theme": "",
|
"color_theme": "",
|
||||||
"comment_options": "",
|
"comment_options": "",
|
||||||
"comments_are_disabled": "",
|
"comments_are_disabled": "",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "",
|
||||||
"confirm_admin_password": "",
|
"confirm_admin_password": "",
|
||||||
"confirm_password": "",
|
"confirm_password": "",
|
||||||
"contain": "",
|
"contain": "",
|
||||||
@@ -298,17 +290,17 @@
|
|||||||
"copy_link_to_clipboard": "",
|
"copy_link_to_clipboard": "",
|
||||||
"copy_password": "",
|
"copy_password": "",
|
||||||
"copy_to_clipboard": "",
|
"copy_to_clipboard": "",
|
||||||
"country": "Land",
|
"country": "",
|
||||||
"cover": "",
|
"cover": "",
|
||||||
"covers": "",
|
"covers": "",
|
||||||
"create": "Erstellen",
|
"create": "",
|
||||||
"create_album": "Album erstellen",
|
"create_album": "",
|
||||||
"create_library": "Bibliothek erstellen",
|
"create_library": "",
|
||||||
"create_link": "Link erstellen",
|
"create_link": "",
|
||||||
"create_link_to_share": "",
|
"create_link_to_share": "",
|
||||||
"create_new_person": "",
|
"create_new_person": "",
|
||||||
"create_new_user": "Neuen Nutzer erstellen",
|
"create_new_user": "",
|
||||||
"create_user": "Nutzer erstellen",
|
"create_user": "",
|
||||||
"created": "",
|
"created": "",
|
||||||
"current_device": "",
|
"current_device": "",
|
||||||
"custom_locale": "",
|
"custom_locale": "",
|
||||||
@@ -318,10 +310,10 @@
|
|||||||
"date_and_time": "",
|
"date_and_time": "",
|
||||||
"date_before": "",
|
"date_before": "",
|
||||||
"date_range": "",
|
"date_range": "",
|
||||||
"day": "Tag",
|
"day": "",
|
||||||
"default_locale": "",
|
"default_locale": "",
|
||||||
"default_locale_description": "",
|
"default_locale_description": "",
|
||||||
"delete": "Löschen",
|
"delete": "",
|
||||||
"delete_album": "",
|
"delete_album": "",
|
||||||
"delete_key": "",
|
"delete_key": "",
|
||||||
"delete_library": "",
|
"delete_library": "",
|
||||||
@@ -329,7 +321,7 @@
|
|||||||
"delete_shared_link": "",
|
"delete_shared_link": "",
|
||||||
"delete_user": "",
|
"delete_user": "",
|
||||||
"deleted_shared_link": "",
|
"deleted_shared_link": "",
|
||||||
"description": "Beschreibung",
|
"description": "",
|
||||||
"details": "",
|
"details": "",
|
||||||
"direction": "",
|
"direction": "",
|
||||||
"disallow_edits": "",
|
"disallow_edits": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,8 +490,6 @@
|
|||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"keep": "Keep",
|
"keep": "Keep",
|
||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"language": "Language",
|
|
||||||
"language_setting_description": "Select your preferred language",
|
|
||||||
"last_seen": "Last seen",
|
"last_seen": "Last seen",
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"let_others_respond": "Let others respond",
|
"let_others_respond": "Let others respond",
|
||||||
@@ -736,7 +734,6 @@
|
|||||||
"template": "Template",
|
"template": "Template",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"theme_selection": "Theme selection",
|
"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",
|
"time_based_memories": "Time-based memories",
|
||||||
"timezone": "Timezone",
|
"timezone": "Timezone",
|
||||||
"toggle_settings": "Toggle settings",
|
"toggle_settings": "Toggle settings",
|
||||||
@@ -787,5 +784,8 @@
|
|||||||
"welcome_to_immich": "Welcome to immich",
|
"welcome_to_immich": "Welcome to immich",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"yes": "Yes",
|
"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"
|
"zoom_image": "Zoom Image"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"actions": "",
|
"actions": "",
|
||||||
"active": "",
|
"active": "",
|
||||||
"activity": "",
|
"activity": "",
|
||||||
"add": "Hozzáadás",
|
"add": "",
|
||||||
"add_a_description": "",
|
"add_a_description": "",
|
||||||
"add_a_location": "",
|
"add_a_location": "",
|
||||||
"add_a_name": "",
|
"add_a_name": "",
|
||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"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_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"zoom_image": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,24 +218,16 @@
|
|||||||
"version_check_enabled_description": "",
|
"version_check_enabled_description": "",
|
||||||
"version_check_settings": "",
|
"version_check_settings": "",
|
||||||
"version_check_settings_description": "",
|
"version_check_settings_description": "",
|
||||||
"video_conversion_job_description": "",
|
"video_conversion_job_description": ""
|
||||||
"notification_email_sent_test_email_button": "",
|
|
||||||
"notification_email_test_email_sent": "",
|
|
||||||
"library_tasks_description": "",
|
|
||||||
"notification_email_test_email_failed": ""
|
|
||||||
},
|
},
|
||||||
"admin_email": "",
|
"admin_email": "",
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"administration": "",
|
"administration": "",
|
||||||
"advanced": "",
|
"advanced": "",
|
||||||
"album_added": "",
|
|
||||||
"album_added_notification_setting_description": "",
|
|
||||||
"album_cover_updated": "",
|
"album_cover_updated": "",
|
||||||
"album_info_updated": "",
|
"album_info_updated": "",
|
||||||
"album_name": "",
|
"album_name": "",
|
||||||
"album_options": "",
|
"album_options": "",
|
||||||
"album_updated": "",
|
|
||||||
"album_updated_setting_description": "",
|
|
||||||
"albums": "",
|
"albums": "",
|
||||||
"all": "",
|
"all": "",
|
||||||
"all_people": "",
|
"all_people": "",
|
||||||
@@ -491,8 +483,6 @@
|
|||||||
"jobs": "",
|
"jobs": "",
|
||||||
"keep": "",
|
"keep": "",
|
||||||
"keyboard_shortcuts": "",
|
"keyboard_shortcuts": "",
|
||||||
"language": "",
|
|
||||||
"language_setting_description": "",
|
|
||||||
"last_seen": "",
|
"last_seen": "",
|
||||||
"leave": "",
|
"leave": "",
|
||||||
"let_others_respond": "",
|
"let_others_respond": "",
|
||||||
@@ -562,10 +552,6 @@
|
|||||||
"no_shared_albums_message": "",
|
"no_shared_albums_message": "",
|
||||||
"not_in_any_album": "",
|
"not_in_any_album": "",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"notification_toggle_setting_description": "",
|
|
||||||
"notifications": "",
|
|
||||||
"notifications_setting_description": "",
|
|
||||||
"oauth": "",
|
|
||||||
"offline": "",
|
"offline": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
"oldest_first": "",
|
"oldest_first": "",
|
||||||
@@ -738,7 +724,6 @@
|
|||||||
"template": "",
|
"template": "",
|
||||||
"theme": "",
|
"theme": "",
|
||||||
"theme_selection": "",
|
"theme_selection": "",
|
||||||
"theme_selection_description": "",
|
|
||||||
"time_based_memories": "",
|
"time_based_memories": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"toggle_settings": "",
|
"toggle_settings": "",
|
||||||
@@ -789,5 +774,8 @@
|
|||||||
"welcome_to_immich": "",
|
"welcome_to_immich": "",
|
||||||
"year": "",
|
"year": "",
|
||||||
"yes": "",
|
"yes": "",
|
||||||
|
"theme_selection_description": "",
|
||||||
|
"language_setting_description": "",
|
||||||
|
"language": "",
|
||||||
"zoom_image": ""
|
"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