From f9db60f25bedd65599d04e70236b63df39c57fb4 Mon Sep 17 00:00:00 2001 From: Tom Graham Date: Sat, 11 Jan 2025 02:18:40 +1100 Subject: [PATCH 001/184] fix(mobile): 15072 Fix issue with boolean filters filtering out results when they shouldn't (#15208) Fix issue with boolean filters filtering out results when they shouldn't. Co-authored-by: Tom graham --- mobile/lib/services/search.service.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 14e53c3ce41af..ba46848cddce0 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -67,9 +67,9 @@ class SearchService { model: filter.camera.model, takenAfter: filter.date.takenAfter, takenBefore: filter.date.takenBefore, - isArchived: filter.display.isArchive, - isFavorite: filter.display.isFavorite, - isNotInAlbum: filter.display.isNotInAlbum, + isArchived: filter.display.isArchive ? true : null, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, personIds: filter.people.map((e) => e.id).toList(), type: type, page: page, @@ -90,9 +90,9 @@ class SearchService { model: filter.camera.model, takenAfter: filter.date.takenAfter, takenBefore: filter.date.takenBefore, - isArchived: filter.display.isArchive, - isFavorite: filter.display.isFavorite, - isNotInAlbum: filter.display.isNotInAlbum, + isArchived: filter.display.isArchive ? true : null, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, personIds: filter.people.map((e) => e.id).toList(), type: type, page: page, From 3030e74fc3b6aa58f8f4f4befc4f455e94bc1a2f Mon Sep 17 00:00:00 2001 From: Jin Xuan <87897838+jinxuan-owyong@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:27:35 +0800 Subject: [PATCH 002/184] fix(web): escape key to clear selection and go to previous page (#15142) (#15219) --- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 5 ++++- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 15 ++++++++++++++- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 15 ++++++++++++++- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 9 ++++++++- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 9 ++++++++- 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0f6c62a5fafaf..4a29f5d8694b3 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -215,7 +215,10 @@ viewMode = AlbumPageViewMode.VIEW; return; } - + if (viewMode === AlbumPageViewMode.SELECT_THUMBNAIL) { + viewMode = AlbumPageViewMode.VIEW; + return; + } if (viewMode === AlbumPageViewMode.SELECT_ASSETS) { await handleCloseSelectAssets(); return; diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5301364ccb116..0e13fbdeb503e 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -28,6 +28,13 @@ const assetStore = new AssetStore({ isArchived: true }); const assetInteraction = new AssetInteraction(); + const handleEscape = () => { + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); + return; + } + }; + onDestroy(() => { assetStore.destroy(); }); @@ -54,7 +61,13 @@ {/if} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 33a03292cd9bf..94436a3dc969f 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,6 +32,13 @@ const assetStore = new AssetStore({ isFavorite: true }); const assetInteraction = new AssetInteraction(); + const handleEscape = () => { + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); + return; + } + }; + onDestroy(() => { assetStore.destroy(); }); @@ -68,7 +75,13 @@ {/if} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 92699f916c3b6..648928d1bdee7 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -24,6 +24,13 @@ const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true }); const assetInteraction = new AssetInteraction(); + const handleEscape = () => { + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); + return; + } + }; + onDestroy(() => { assetStore.destroy(); }); @@ -51,5 +58,5 @@ {/snippet} {/if} - + diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7f97d3772b973..686d82b1251d0 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -87,6 +87,13 @@ } }; + const handleEscape = () => { + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); + return; + } + }; + onDestroy(() => { assetStore.destroy(); }); @@ -122,7 +129,7 @@ {/snippet} - +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

From 930f979960293b4e040a37e59562751a96604a42 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 10 Jan 2025 14:02:12 -0500 Subject: [PATCH 003/184] feat: migration api keys to use kysely (#15206) --- server/src/dtos/auth.dto.ts | 4 +- server/src/interfaces/api-key.interface.ts | 7 +- server/src/queries/api.key.repository.sql | 122 ++++++++---------- server/src/repositories/api-key.repository.ts | 93 +++++++++---- server/src/services/api-key.service.spec.ts | 5 +- server/src/services/auth.service.spec.ts | 8 +- server/src/services/auth.service.ts | 2 +- server/src/types.ts | 9 ++ server/test/fixtures/api-key.stub.ts | 8 ++ 9 files changed, 151 insertions(+), 107 deletions(-) create mode 100644 server/src/types.ts diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index b2bf1b8bccc86..d6b73f584a8de 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; -import { APIKeyEntity } from 'src/entities/api-key.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; import { ImmichCookie } from 'src/enum'; +import { AuthApiKey } from 'src/types'; import { toEmail } from 'src/validation'; export type CookieResponse = { @@ -16,7 +16,7 @@ export type CookieResponse = { export class AuthDto { user!: UserEntity; - apiKey?: APIKeyEntity; + apiKey?: AuthApiKey; sharedLink?: SharedLinkEntity; session?: SessionEntity; } diff --git a/server/src/interfaces/api-key.interface.ts b/server/src/interfaces/api-key.interface.ts index 731b7ff6fbb75..473a2b8019a55 100644 --- a/server/src/interfaces/api-key.interface.ts +++ b/server/src/interfaces/api-key.interface.ts @@ -1,16 +1,19 @@ +import { Insertable } from 'kysely'; +import { ApiKeys } from 'src/db'; import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { AuthApiKey } from 'src/types'; export const IKeyRepository = 'IKeyRepository'; export interface IKeyRepository { - create(dto: Partial): Promise; + create(dto: Insertable): Promise; update(userId: string, id: string, dto: Partial): Promise; delete(userId: string, id: string): Promise; /** * Includes the hashed `key` for verification * @param id */ - getKey(hashedToken: string): Promise; + getKey(hashedToken: string): Promise; getById(userId: string, id: string): Promise; getByUserId(userId: string): Promise; } diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index f4989d355e880..e1ed8a3dd614a 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -1,77 +1,59 @@ -- NOTE: This file is auto generated by ./sql-generator -- ApiKeyRepository.getKey -SELECT DISTINCT - "distinctAlias"."APIKeyEntity_id" AS "ids_APIKeyEntity_id" -FROM - ( - SELECT - "APIKeyEntity"."id" AS "APIKeyEntity_id", - "APIKeyEntity"."key" AS "APIKeyEntity_key", - "APIKeyEntity"."userId" AS "APIKeyEntity_userId", - "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", - "APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id", - "APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name", - "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", - "APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email", - "APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel", - "APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId", - "APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath", - "APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword", - "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt", - "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt", - "APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status", - "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", - "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", - "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes", - "APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt", - "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId", - "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key", - "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value" - FROM - "api_keys" "APIKeyEntity" - LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId" - AND ( - "APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL - ) - LEFT JOIN "user_metadata" "7f5f7a38bf327bfbbf826778460704c9a50fe6f4" ON "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" = "APIKeyEntity__APIKeyEntity_user"."id" - WHERE - (("APIKeyEntity"."key" = $1)) - ) "distinctAlias" -ORDER BY - "APIKeyEntity_id" ASC -LIMIT - 1 +select + "api_keys"."id", + "api_keys"."key", + "api_keys"."userId", + "api_keys"."permissions", + to_json("user") as "user" +from + "api_keys" + inner join lateral ( + select + "users".*, + ( + select + array_agg("user_metadata") as "metadata" + from + "user_metadata" + where + "users"."id" = "user_metadata"."userId" + ) as "metadata" + from + "users" + where + "users"."id" = "api_keys"."userId" + and "users"."deletedAt" is null + ) as "user" on true +where + "api_keys"."key" = $1 -- ApiKeyRepository.getById -SELECT - "APIKeyEntity"."id" AS "APIKeyEntity_id", - "APIKeyEntity"."name" AS "APIKeyEntity_name", - "APIKeyEntity"."userId" AS "APIKeyEntity_userId", - "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", - "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", - "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" -FROM - "api_keys" "APIKeyEntity" -WHERE - ( - ("APIKeyEntity"."userId" = $1) - AND ("APIKeyEntity"."id" = $2) - ) -LIMIT - 1 +select + "id", + "name", + "userId", + "createdAt", + "updatedAt", + "permissions" +from + "api_keys" +where + "id" = $1::uuid + and "userId" = $2 -- ApiKeyRepository.getByUserId -SELECT - "APIKeyEntity"."id" AS "APIKeyEntity_id", - "APIKeyEntity"."name" AS "APIKeyEntity_name", - "APIKeyEntity"."userId" AS "APIKeyEntity_userId", - "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", - "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", - "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" -FROM - "api_keys" "APIKeyEntity" -WHERE - (("APIKeyEntity"."userId" = $1)) -ORDER BY - "APIKeyEntity"."createdAt" DESC +select + "id", + "name", + "userId", + "createdAt", + "updatedAt", + "permissions" +from + "api_keys" +where + "userId" = $1 +order by + "createdAt" desc diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index bb37390de1df3..c0fc7532137c2 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -1,52 +1,97 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { ApiKeys, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { AuthApiKey } from 'src/types'; +import { asUuid } from 'src/utils/database'; import { Repository } from 'typeorm'; +const columns = ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'] as const; + @Injectable() export class ApiKeyRepository implements IKeyRepository { - constructor(@InjectRepository(APIKeyEntity) private repository: Repository) {} + constructor( + @InjectRepository(APIKeyEntity) private repository: Repository, + @InjectKysely() private db: Kysely, + ) {} + + async create(dto: Insertable): Promise { + const { id, name, createdAt, updatedAt, permissions } = await this.db + .insertInto('api_keys') + .values(dto) + .returningAll() + .executeTakeFirstOrThrow(); - async create(dto: Partial): Promise { - return this.repository.save(dto); + return { id, name, createdAt, updatedAt, permissions } as APIKeyEntity; } - async update(userId: string, id: string, dto: Partial): Promise { - await this.repository.update({ userId, id }, dto); - return this.repository.findOneOrFail({ where: { id: dto.id } }); + async update(userId: string, id: string, dto: Updateable): Promise { + return this.db + .updateTable('api_keys') + .set(dto) + .where('api_keys.userId', '=', userId) + .where('id', '=', asUuid(id)) + .returningAll() + .executeTakeFirstOrThrow() as unknown as Promise; } async delete(userId: string, id: string): Promise { - await this.repository.delete({ userId, id }); + await this.db.deleteFrom('api_keys').where('userId', '=', userId).where('id', '=', asUuid(id)).execute(); } @GenerateSql({ params: [DummyValue.STRING] }) - getKey(hashedToken: string): Promise { - return this.repository.findOne({ - select: { - id: true, - key: true, - userId: true, - permissions: true, - }, - where: { key: hashedToken }, - relations: { - user: { - metadata: true, - }, - }, - }); + getKey(hashedToken: string): Promise { + return this.db + .selectFrom('api_keys') + .innerJoinLateral( + (eb) => + eb + .selectFrom('users') + .selectAll('users') + .select((eb) => + eb + .selectFrom('user_metadata') + .whereRef('users.id', '=', 'user_metadata.userId') + .select((eb) => eb.fn('array_agg', [eb.table('user_metadata')]).as('metadata')) + .as('metadata'), + ) + .whereRef('users.id', '=', 'api_keys.userId') + .where('users.deletedAt', 'is', null) + .as('user'), + (join) => join.onTrue(), + ) + .select((eb) => [ + 'api_keys.id', + 'api_keys.key', + 'api_keys.userId', + 'api_keys.permissions', + eb.fn.toJson('user').as('user'), + ]) + .where('api_keys.key', '=', hashedToken) + .executeTakeFirst() as Promise; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getById(userId: string, id: string): Promise { - return this.repository.findOne({ where: { userId, id } }); + return this.db + .selectFrom('api_keys') + .select(columns) + .where('id', '=', asUuid(id)) + .where('userId', '=', userId) + .executeTakeFirst() as unknown as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string): Promise { - return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); + return this.db + .selectFrom('api_keys') + .select(columns) + .where('userId', '=', userId) + .orderBy('createdAt', 'desc') + .execute() as unknown as Promise; } } diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 3841ba1be9756..8d07985440ae0 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -49,10 +49,7 @@ describe(APIKeyService.name, () => { it('should throw an error if the api key does not have sufficient permissions', async () => { await expect( - sut.create( - { ...authStub.admin, apiKey: { ...keyStub.admin, permissions: [] } }, - { permissions: [Permission.ASSET_READ] }, - ), + sut.create({ ...authStub.admin, apiKey: keyStub.authKey }, { permissions: [Permission.ASSET_READ] }), ).rejects.toBeInstanceOf(BadRequestException); }); }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index d34e2673f56ef..06035b03a2937 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -405,7 +405,7 @@ describe('AuthService', () => { describe('validate - api key', () => { it('should throw an error if no api key is found', async () => { - keyMock.getKey.mockResolvedValue(null); + keyMock.getKey.mockResolvedValue(void 0); await expect( sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, @@ -417,7 +417,7 @@ describe('AuthService', () => { }); it('should throw an error if api key has insufficient permissions', async () => { - keyMock.getKey.mockResolvedValue({ ...keyStub.admin, permissions: [] }); + keyMock.getKey.mockResolvedValue(keyStub.authKey); await expect( sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, @@ -428,14 +428,14 @@ describe('AuthService', () => { }); it('should return an auth dto', async () => { - keyMock.getKey.mockResolvedValue(keyStub.admin); + keyMock.getKey.mockResolvedValue(keyStub.authKey); await expect( sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, queryParams: {}, metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), - ).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); + ).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.authKey }); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 0d44fa0562235..d6154976f9ffe 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -308,7 +308,7 @@ export class AuthService extends BaseService { private async validateApiKey(key: string): Promise { const hashedKey = this.cryptoRepository.hashSha256(key); const apiKey = await this.keyRepository.getKey(hashedKey); - if (apiKey?.user) { + if (apiKey) { return { user: apiKey.user, apiKey }; } diff --git a/server/src/types.ts b/server/src/types.ts new file mode 100644 index 0000000000000..c55de4160dd15 --- /dev/null +++ b/server/src/types.ts @@ -0,0 +1,9 @@ +import { UserEntity } from 'src/entities/user.entity'; +import { Permission } from 'src/enum'; + +export type AuthApiKey = { + id: string; + key: string; + user: UserEntity; + permissions: Permission[]; +}; diff --git a/server/test/fixtures/api-key.stub.ts b/server/test/fixtures/api-key.stub.ts index f8b1832c84e37..248d30c2eccda 100644 --- a/server/test/fixtures/api-key.stub.ts +++ b/server/test/fixtures/api-key.stub.ts @@ -1,8 +1,16 @@ import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { AuthApiKey } from 'src/types'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; export const keyStub = { + authKey: Object.freeze({ + id: 'my-random-guid', + key: 'my-api-key (hashed)', + user: userStub.admin, + permissions: [], + } as AuthApiKey), + admin: Object.freeze({ id: 'my-random-guid', name: 'My Key', From cc6a8b0c74a9e16e9697eee967b86a80ea9a3b36 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 10 Jan 2025 14:20:15 -0500 Subject: [PATCH 004/184] refactor: migrate system metadata to kysely (#15231) --- .../queries/system.metadata.repository.sql | 24 ++++++++++++++ .../system-metadata.repository.ts | 33 +++++++++++++------ 2 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 server/src/queries/system.metadata.repository.sql diff --git a/server/src/queries/system.metadata.repository.sql b/server/src/queries/system.metadata.repository.sql new file mode 100644 index 0000000000000..996ac14803092 --- /dev/null +++ b/server/src/queries/system.metadata.repository.sql @@ -0,0 +1,24 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SystemMetadataRepository.get +select + "value" +from + "system_metadata" +where + "key" = $1 + +-- SystemMetadataRepository.set +insert into + "system_metadata" ("key", "value") +values + ($1, $2) +on conflict ("key") do +update +set + "value" = $3 + +-- SystemMetadataRepository.delete +delete from "system_metadata" +where + "key" = $1 diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts index 1c6aaf0517804..7cd4d715e2fec 100644 --- a/server/src/repositories/system-metadata.repository.ts +++ b/server/src/repositories/system-metadata.repository.ts @@ -1,31 +1,44 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; import { readFile } from 'node:fs/promises'; -import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity'; +import { DB, SystemMetadata as DbSystemMetadata } from 'src/db'; +import { GenerateSql } from 'src/decorators'; +import { SystemMetadata } from 'src/entities/system-metadata.entity'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { Repository } from 'typeorm'; + +type Upsert = Insertable; @Injectable() export class SystemMetadataRepository implements ISystemMetadataRepository { - constructor( - @InjectRepository(SystemMetadataEntity) - private repository: Repository, - ) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: ['metadata_key'] }) async get(key: T): Promise { - const metadata = await this.repository.findOne({ where: { key } }); + const metadata = await this.db + .selectFrom('system_metadata') + .select('value') + .where('key', '=', key) + .executeTakeFirst(); + if (!metadata) { return null; } return metadata.value as SystemMetadata[T]; } + @GenerateSql({ params: ['metadata_key', { foo: 'bar' }] }) async set(key: T, value: SystemMetadata[T]): Promise { - await this.repository.upsert({ key, value }, { conflictPaths: { key: true } }); + await this.db + .insertInto('system_metadata') + .values({ key, value } as Upsert) + .onConflict((oc) => oc.columns(['key']).doUpdateSet({ value } as Upsert)) + .execute(); } + @GenerateSql({ params: ['metadata_key'] }) async delete(key: T): Promise { - await this.repository.delete({ key }); + await this.db.deleteFrom('system_metadata').where('key', '=', key).execute(); } readFile(filename: string): Promise { From e51091b6e58ef73b36bcea42e13b7b6277656889 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 10 Jan 2025 18:48:21 -0500 Subject: [PATCH 005/184] refactor: migrate trash to kysely (#15233) --- server/src/interfaces/trash.interface.ts | 4 +- server/src/queries/trash.repository.sql | 27 ++++++++ server/src/repositories/trash.repository.ts | 72 ++++++++++----------- server/src/services/trash.service.spec.ts | 17 +++-- server/src/services/trash.service.ts | 43 +++++++----- 5 files changed, 102 insertions(+), 61 deletions(-) create mode 100644 server/src/queries/trash.repository.sql diff --git a/server/src/interfaces/trash.interface.ts b/server/src/interfaces/trash.interface.ts index 96c2322d8afd6..38e7c523ce613 100644 --- a/server/src/interfaces/trash.interface.ts +++ b/server/src/interfaces/trash.interface.ts @@ -1,10 +1,8 @@ -import { Paginated, PaginationOptions } from 'src/utils/pagination'; - export const ITrashRepository = 'ITrashRepository'; export interface ITrashRepository { empty(userId: string): Promise; restore(userId: string): Promise; restoreAll(assetIds: string[]): Promise; - getDeletedIds(pagination: PaginationOptions): Paginated; + getDeletedIds(): AsyncIterableIterator<{ id: string }>; } diff --git a/server/src/queries/trash.repository.sql b/server/src/queries/trash.repository.sql new file mode 100644 index 0000000000000..77c2ea51d0d4d --- /dev/null +++ b/server/src/queries/trash.repository.sql @@ -0,0 +1,27 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- TrashRepository.restore +update "assets" +set + "status" = $1, + "deletedAt" = $2 +where + "ownerId" = $3 + and "status" = $4 + +-- TrashRepository.empty +update "assets" +set + "status" = $1 +where + "ownerId" = $2 + and "status" = $3 + +-- TrashRepository.restoreAll +update "assets" +set + "status" = $1, + "deletedAt" = $2 +where + "status" = $3 + and "id" in ($4) diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts index d24f4f709afac..c1db31a3db38d 100644 --- a/server/src/repositories/trash.repository.ts +++ b/server/src/repositories/trash.repository.ts @@ -1,52 +1,50 @@ -import { InjectRepository } from '@nestjs/typeorm'; -import { AssetEntity } from 'src/entities/asset.entity'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetStatus } from 'src/enum'; import { ITrashRepository } from 'src/interfaces/trash.interface'; -import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination'; -import { In, Repository } from 'typeorm'; export class TrashRepository implements ITrashRepository { - constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} - - async getDeletedIds(pagination: PaginationOptions): Paginated { - const { hasNextPage, items } = await paginatedBuilder( - this.assetRepository - .createQueryBuilder('asset') - .select('asset.id') - .where({ status: AssetStatus.DELETED }) - .withDeleted(), - pagination, - ); - - return { - hasNextPage, - items: items.map((asset) => asset.id), - }; + constructor(@InjectKysely() private db: Kysely) {} + + getDeletedIds(): AsyncIterableIterator<{ id: string }> { + return this.db.selectFrom('assets').select(['id']).where('status', '=', AssetStatus.DELETED).stream(); } + @GenerateSql({ params: [DummyValue.UUID] }) async restore(userId: string): Promise { - const result = await this.assetRepository.update( - { ownerId: userId, status: AssetStatus.TRASHED }, - { status: AssetStatus.ACTIVE, deletedAt: null }, - ); - - return result.affected || 0; + const { numUpdatedRows } = await this.db + .updateTable('assets') + .where('ownerId', '=', userId) + .where('status', '=', AssetStatus.TRASHED) + .set({ status: AssetStatus.ACTIVE, deletedAt: null }) + .executeTakeFirst(); + + return Number(numUpdatedRows); } + @GenerateSql({ params: [DummyValue.UUID] }) async empty(userId: string): Promise { - const result = await this.assetRepository.update( - { ownerId: userId, status: AssetStatus.TRASHED }, - { status: AssetStatus.DELETED }, - ); - - return result.affected || 0; + const { numUpdatedRows } = await this.db + .updateTable('assets') + .where('ownerId', '=', userId) + .where('status', '=', AssetStatus.TRASHED) + .set({ status: AssetStatus.DELETED }) + .executeTakeFirst(); + + return Number(numUpdatedRows); } + @GenerateSql({ params: [[DummyValue.UUID]] }) async restoreAll(ids: string[]): Promise { - const result = await this.assetRepository.update( - { id: In(ids), status: AssetStatus.TRASHED }, - { status: AssetStatus.ACTIVE, deletedAt: null }, - ); - return result.affected ?? 0; + const { numUpdatedRows } = await this.db + .updateTable('assets') + .where('status', '=', AssetStatus.TRASHED) + .where('id', 'in', ids) + .set({ status: AssetStatus.ACTIVE, deletedAt: null }) + .executeTakeFirst(); + + return Number(numUpdatedRows); } } diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index 748faa14abdc2..4d877c9dfa469 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -7,6 +7,13 @@ import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock' import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; +async function* makeAssetIdStream(count: number): AsyncIterableIterator<{ id: string }> { + for (let i = 0; i < count; i++) { + await Promise.resolve(); + yield { id: `asset-${i + 1}` }; + } +} + describe(TrashService.name, () => { let sut: TrashService; @@ -48,14 +55,14 @@ describe(TrashService.name, () => { describe('restore', () => { it('should handle an empty trash', async () => { - trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false }); + trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(0)); trashMock.restore.mockResolvedValue(0); await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 }); expect(trashMock.restore).toHaveBeenCalledWith('user-id'); }); it('should restore', async () => { - trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(1)); trashMock.restore.mockResolvedValue(1); await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 }); expect(trashMock.restore).toHaveBeenCalledWith('user-id'); @@ -64,14 +71,14 @@ describe(TrashService.name, () => { describe('empty', () => { it('should handle an empty trash', async () => { - trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false }); + trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(0)); trashMock.empty.mockResolvedValue(0); await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 }); expect(jobMock.queue).not.toHaveBeenCalled(); }); it('should empty the trash', async () => { - trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(1)); trashMock.empty.mockResolvedValue(1); await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 }); expect(trashMock.empty).toHaveBeenCalledWith('user-id'); @@ -88,7 +95,7 @@ describe(TrashService.name, () => { describe('handleQueueEmptyTrash', () => { it('should queue asset delete jobs', async () => { - trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + trashMock.getDeletedIds.mockReturnValue(makeAssetIdStream(1)); await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ { diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 8136ff4c7e293..d66461ef94b0b 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -5,7 +5,6 @@ import { TrashResponseDto } from 'src/dtos/trash.dto'; import { Permission } from 'src/enum'; import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface'; import { BaseService } from 'src/services/base.service'; -import { usePagination } from 'src/utils/pagination'; export class TrashService extends BaseService { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { @@ -46,27 +45,39 @@ export class TrashService extends BaseService { @OnJob({ name: JobName.QUEUE_TRASH_EMPTY, queue: QueueName.BACKGROUND_TASK }) async handleQueueEmptyTrash() { + const assets = this.trashRepository.getDeletedIds(); + let count = 0; - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.trashRepository.getDeletedIds(pagination), - ); + const batch: string[] = []; + for await (const { id } of assets) { + batch.push(id); - for await (const assetIds of assetPagination) { - this.logger.debug(`Queueing ${assetIds.length} asset(s) for deletion from the trash`); - count += assetIds.length; - await this.jobRepository.queueAll( - assetIds.map((assetId) => ({ - name: JobName.ASSET_DELETION, - data: { - id: assetId, - deleteOnDisk: true, - }, - })), - ); + if (batch.length === JOBS_ASSET_PAGINATION_SIZE) { + await this.handleBatch(batch); + count += batch.length; + batch.length = 0; + } } + await this.handleBatch(batch); + count += batch.length; + batch.length = 0; + this.logger.log(`Queued ${count} asset(s) for deletion from the trash`); return JobStatus.SUCCESS; } + + private async handleBatch(ids: string[]) { + this.logger.debug(`Queueing ${ids.length} asset(s) for deletion from the trash`); + await this.jobRepository.queueAll( + ids.map((assetId) => ({ + name: JobName.ASSET_DELETION, + data: { + id: assetId, + deleteOnDisk: true, + }, + })), + ); + } } From beb31cebed341203aa9125e01fd0706b9bfca5ce Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:20:56 -0500 Subject: [PATCH 006/184] fix(mobile): don't crash android app when video player throws exception (#15236) update commit ref --- mobile/pubspec.lock | 4 ++-- mobile/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 34eb217828102..80b691baa09e8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1012,8 +1012,8 @@ packages: dependency: "direct main" description: path: "." - ref: ac78487 - resolved-ref: ac78487b9a87c9e72cd15b428270a905ac551f29 + ref: "4530808" + resolved-ref: "4530808a6d04c9992de184c423c9e87fbf6a53eb" url: "https://github.com/immich-app/native_video_player" source: git version: "1.3.1" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c69290b29913b..12433e0bfcad6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: native_video_player: git: url: https://github.com/immich-app/native_video_player - ref: ac78487 + ref: '4530808' #image editing packages crop_image: ^1.0.13 From cab201270c6cf17d391e24e832b335f9ce42e07a Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 11 Jan 2025 21:12:34 +0100 Subject: [PATCH 007/184] chore: migrate version-history repository to kysely (#15267) * chore: generate sql for version-history repository * chore: run kysely-codegen * chore: migrate version-history repository to kysely * fix: change `| null` to `| undefined` * chore: clean up unneeded async --- server/src/db.d.ts | 19 ++++++++-------- .../interfaces/version-history.interface.ts | 2 +- .../queries/version.history.repository.sql | 17 ++++++++++++++ .../version-history.repository.ts | 22 +++++++++++-------- 4 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 server/src/queries/version.history.repository.sql diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 454c5176de110..a5cab5dab7a06 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -62,6 +62,7 @@ export interface Albums { export interface AlbumsAssetsAssets { albumsId: string; assetsId: string; + createdAt: Generated; } export interface AlbumsSharedUsersUsers { @@ -201,7 +202,6 @@ export interface GeodataPlaces { admin2Name: string | null; alternateNames: string | null; countryCode: string; - earthCoord: Generated; id: number; latitude: number; longitude: number; @@ -257,7 +257,7 @@ export interface NaturalearthCountries { admin: string; admin_a3: string; coordinates: string; - id: Generated; + id: number; type: string; } @@ -311,13 +311,6 @@ export interface SharedLinks { userId: string; } -export interface SmartInfo { - assetId: string; - objects: string[] | null; - smartInfoTextSearchableColumn: Generated; - tags: string[] | null; -} - export interface SmartSearch { assetId: string; embedding: string; @@ -399,6 +392,12 @@ export interface VectorsPgVectorIndexStat { tablerelid: number | null; } +export interface VersionHistory { + createdAt: Generated; + id: Generated; + version: string; +} + export interface DB { activity: Activity; albums: Albums; @@ -425,7 +424,6 @@ export interface DB { sessions: Sessions; shared_link__asset: SharedLinkAsset; shared_links: SharedLinks; - smart_info: SmartInfo; smart_search: SmartSearch; socket_io_attachments: SocketIoAttachments; system_config: SystemConfig; @@ -436,4 +434,5 @@ export interface DB { user_metadata: UserMetadata; users: Users; 'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat; + version_history: VersionHistory; } diff --git a/server/src/interfaces/version-history.interface.ts b/server/src/interfaces/version-history.interface.ts index 67337062200f0..c38552c24ff4e 100644 --- a/server/src/interfaces/version-history.interface.ts +++ b/server/src/interfaces/version-history.interface.ts @@ -5,5 +5,5 @@ export const IVersionHistoryRepository = 'IVersionHistoryRepository'; export interface IVersionHistoryRepository { create(version: Omit): Promise; getAll(): Promise; - getLatest(): Promise; + getLatest(): Promise; } diff --git a/server/src/queries/version.history.repository.sql b/server/src/queries/version.history.repository.sql new file mode 100644 index 0000000000000..2e898cac31f86 --- /dev/null +++ b/server/src/queries/version.history.repository.sql @@ -0,0 +1,17 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- VersionHistoryRepository.getAll +select + * +from + "version_history" +order by + "createdAt" desc + +-- VersionHistoryRepository.getLatest +select + * +from + "version_history" +order by + "createdAt" desc diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts index e32ceaf4e9ec0..a5016873508cd 100644 --- a/server/src/repositories/version-history.repository.ts +++ b/server/src/repositories/version-history.repository.ts @@ -1,23 +1,27 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { VersionHistoryEntity } from 'src/entities/version-history.entity'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; -import { Repository } from 'typeorm'; @Injectable() export class VersionHistoryRepository implements IVersionHistoryRepository { - constructor(@InjectRepository(VersionHistoryEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} - async getAll(): Promise { - return this.repository.find({ order: { createdAt: 'DESC' } }); + @GenerateSql() + getAll(): Promise { + return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute(); } - async getLatest(): Promise { - const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 }); - return results[0] || null; + @GenerateSql() + getLatest(): Promise { + return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); } + @GenerateSql({ params: [DummyValue.STRING] }) create(version: Omit): Promise { - return this.repository.save(version); + return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow(); } } From 0b8cfc6b820ce4007bfa25a6813a1825bc6004ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:00:50 -0500 Subject: [PATCH 008/184] chore(deps): update base-image to v20250107 (major) (#15251) chore(deps): update base-image to v20250107 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 4c1aecb8fa545..e4fb6d43521eb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20241224@sha256:6832c632c2a8cba5e20053ab694c9a8080e621841c784ed5d4675ef9dd203588 AS dev +FROM ghcr.io/immich-app/base-server-dev:20250107@sha256:d00ab37e1c1ed87b799d6509fbc825a721ca0723c59c67955217826882017d38 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -42,7 +42,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20241224@sha256:69da007c241a961d6927d3d03f1c83ef0ec5c70bf656bff3ced32546a777e6f6 +FROM ghcr.io/immich-app/base-server-prod:20250107@sha256:78e92f113103271d43a3b050370b21b31c3c14792d3d23b18b542581a440c72b WORKDIR /usr/src/app ENV NODE_ENV=production \ From 2f9a66e961571afe7d7f882fe3e4872c95a758dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:02:12 -0600 Subject: [PATCH 009/184] chore(config): migrate renovate config (#15262) chore(config): migrate config renovate.json Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- renovate.json | 103 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/renovate.json b/renovate.json index 39e0e7f811f02..dd3ca1ad59e3e 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,9 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", "docker:pinDigests"], + "extends": [ + "config:recommended", + "docker:pinDigests" + ], "minimumReleaseAge": "5 days", "packageRules": [ { @@ -13,69 +16,109 @@ "web/**" ], "groupName": "typescript-projects", - "matchUpdateTypes": ["minor", "patch"], - "excludePackagePrefixes": ["exiftool", "reflect-metadata"], - "excludePackageNames": ["node", "@types/node", "@mapbox/mapbox-gl-rtl-text"], - "schedule": "on tuesday" + "matchUpdateTypes": [ + "minor", + "patch" + ], + "schedule": "on tuesday", + "matchPackageNames": [ + "!exiftool{/,}**", + "!reflect-metadata{/,}**", + "!node", + "!@types/node", + "!@mapbox/mapbox-gl-rtl-text" + ] }, { - "matchFileNames": ["machine-learning/**"], + "matchFileNames": [ + "machine-learning/**" + ], "groupName": "machine-learning", "rangeStrategy": "in-range-only", "schedule": "on tuesday" }, { - "matchFileNames": ["mobile/**"], + "matchFileNames": [ + "mobile/**" + ], "groupName": "mobile", - "matchUpdateTypes": ["minor", "patch"], + "matchUpdateTypes": [ + "minor", + "patch" + ], "schedule": "on tuesday", - "addLabels": ["📱mobile"] + "addLabels": [ + "📱mobile" + ] }, { "groupName": "flutter", - "matchPackagePatterns": ["flutter"], - "schedule": "on tuesday" + "schedule": "on tuesday", + "matchPackageNames": [ + "/flutter/" + ] }, { "groupName": "exiftool", - "matchPackagePrefixes": ["exiftool"], - "schedule": "on tuesday" + "schedule": "on tuesday", + "matchPackageNames": [ + "exiftool{/,}**" + ] }, { "groupName": "svelte", - "matchUpdateTypes": ["major"], - "matchPackagePrefixes": ["@sveltejs"], - "schedule": "on tuesday" + "matchUpdateTypes": [ + "major" + ], + "schedule": "on tuesday", + "matchPackageNames": [ + "@sveltejs{/,}**" + ] }, { - "matchFileNames": [".github/**"], + "matchFileNames": [ + ".github/**" + ], "groupName": "github-actions", "schedule": "on tuesday" }, { "groupName": "base-image", - "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"], - "minimumReleaseAge": "0" + "minimumReleaseAge": "0", + "matchPackageNames": [ + "ghcr.io/immich-app/base-server{/,}**" + ] }, { - "matchDatasources": ["npm"], + "matchDatasources": [ + "npm" + ], "rangeStrategy": "bump", "groupName": "node", "versioning": "node", - "matchPackageNames": ["node", "@types/node"], + "matchPackageNames": [ + "node", + "@types/node" + ], "schedule": "on tuesday" }, { "groupName": "node", - "matchDatasources": ["docker"], - "matchPackageNames": ["node"], + "matchDatasources": [ + "docker" + ], + "matchPackageNames": [ + "node" + ], "versionCompatibility": "^(?[^-]+)(?-.*)?$", "versioning": "node", "schedule": "on tuesday" }, { - "packageNames": ["com.google.guava:guava"], - "versionScheme": "docker", + "matchPackageNames": [ + "com.google.guava:guava" + ], + "versioning": "docker", "schedule": "on tuesday" } ], @@ -84,6 +127,12 @@ "mobile/ios", "mobile/android" ], - "ignoreDeps": ["http", "intl"], - "labels": ["dependencies", "changelog:skip"] + "ignoreDeps": [ + "http", + "intl" + ], + "labels": [ + "dependencies", + "changelog:skip" + ] } From 2301affd7e60f2588b7166d822e845723f3d4e76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:04:22 -0600 Subject: [PATCH 010/184] chore(deps): update node.js to v22.13.0 (#15249) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/.nvmrc | 2 +- cli/package.json | 2 +- docs/.nvmrc | 2 +- docs/package.json | 2 +- e2e/.nvmrc | 2 +- e2e/package.json | 2 +- open-api/typescript-sdk/.nvmrc | 2 +- open-api/typescript-sdk/package.json | 2 +- server/.nvmrc | 2 +- server/package.json | 2 +- web/.nvmrc | 2 +- web/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/.nvmrc b/cli/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/cli/package.json b/cli/package.json index da3e359ea8631..ddbdcec0a202f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -67,6 +67,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } diff --git a/docs/.nvmrc b/docs/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/docs/package.json b/docs/package.json index e739cd68c74c5..a77a41656ad87 100644 --- a/docs/package.json +++ b/docs/package.json @@ -55,6 +55,6 @@ "node": ">=20" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/e2e/package.json b/e2e/package.json index efeba2f073c38..54224211246be 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -53,6 +53,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index f4642ed71de00..51fd621e72758 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } diff --git a/server/.nvmrc b/server/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/server/package.json b/server/package.json index a8c911c8a6f97..b9fa78a3236a1 100644 --- a/server/package.json +++ b/server/package.json @@ -143,6 +143,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } diff --git a/web/.nvmrc b/web/.nvmrc index 1d9b7831ba9d9..6fa8dec4cd678 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -22.12.0 +22.13.0 diff --git a/web/package.json b/web/package.json index a1e96a4e09f7e..7ceae0a0ae220 100644 --- a/web/package.json +++ b/web/package.json @@ -87,6 +87,6 @@ "thumbhash": "^0.1.1" }, "volta": { - "node": "22.12.0" + "node": "22.13.0" } } From 2b7611201414e1bfbbb32ad5c2df1b1d0923c964 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:04:40 -0600 Subject: [PATCH 011/184] chore(deps): update github-actions (#15248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cli.yml | 4 ++-- .github/workflows/docker.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index e3b2d68435146..b6371d4a662b9 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.8.0 @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2fac92c4e84ae..7ae51696e64b4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -122,7 +122,7 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.8.0 @@ -174,7 +174,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: context: ${{ env.context }} file: ${{ env.file }} @@ -213,7 +213,7 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.8.0 @@ -265,7 +265,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 with: context: ${{ env.context }} file: ${{ env.file }} From 581d32269dca356d10d27bf9b60420b6153c2dec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:05:20 -0600 Subject: [PATCH 012/184] fix(deps): update machine-learning (#15247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 4 +- machine-learning/poetry.lock | 290 ++++++++++++++++++----------------- 2 files changed, 150 insertions(+), 144 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index bca9244fa111b..705e4827ff1e0 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:2c80c66d876952e04fa74113864903198b7cfb36b839acb7a8fef82e94ed067c AS builder-cpu +FROM python:3.11-bookworm@sha256:b337e1fd27dbacda505219f713789bf82766694095876769ea10c2d34b4f470b AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:370c586a6ffc8c619e6d652f81c094b34b14b8f2fb9251f092de23f16e299b78 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:873952659a04188d2a62d5f7e30fd673d2559432a847a8ad5fcaf9cbd085e9ed AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index eb8fe31dffcf7..33a4354c3065e 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.27.0" +version = "0.27.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"}, - {file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"}, + {file = "huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec"}, + {file = "huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b"}, ] [package.dependencies] @@ -1625,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.32.4" +version = "2.32.5" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.32.4-py3-none-any.whl", hash = "sha256:7c5b8767c0d771b5167d5d6b82878622faead74f394eb9cafe8891d89eb36b97"}, - {file = "locust-2.32.4.tar.gz", hash = "sha256:fd650cbc40842e721668a8d0f7f8224775432b40c63d0a378546b9a9f54b7559"}, + {file = "locust-2.32.5-py3-none-any.whl", hash = "sha256:2f49509868ffc2e368be40921c6825f92147c84e997206760a85dab3058f5efb"}, + {file = "locust-2.32.5.tar.gz", hash = "sha256:ea7bc1e8ce2520e8893c471b4b0a56a4f53b01b4b618adfe8d2c8ab2728b5821"}, ] [package.dependencies] @@ -1893,49 +1893,55 @@ files = [ [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -2183,86 +2189,86 @@ numpy = [ [[package]] name = "orjson" -version = "3.10.12" +version = "3.10.14" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"}, - {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"}, - {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"}, - {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"}, - {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"}, - {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"}, - {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"}, - {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"}, - {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"}, - {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"}, - {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"}, - {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"}, - {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"}, - {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"}, - {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"}, - {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"}, - {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"}, - {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"}, - {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"}, - {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"}, - {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"}, - {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"}, - {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"}, - {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"}, - {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"}, - {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"}, - {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"}, - {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"}, - {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"}, - {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"}, + {file = "orjson-3.10.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:849ea7845a55f09965826e816cdc7689d6cf74fe9223d79d758c714af955bcb6"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5947b139dfa33f72eecc63f17e45230a97e741942955a6c9e650069305eb73d"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cde6d76910d3179dae70f164466692f4ea36da124d6fb1a61399ca589e81d69a"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6dfbaeb7afa77ca608a50e2770a0461177b63a99520d4928e27591b142c74b1"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa45e489ef80f28ff0e5ba0a72812b8cfc7c1ef8b46a694723807d1b07c89ebb"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5007abfdbb1d866e2aa8990bd1c465f0f6da71d19e695fc278282be12cffa5"}, + {file = "orjson-3.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b49e2af011c84c3f2d541bb5cd1e3c7c2df672223e7e3ea608f09cf295e5f8a"}, + {file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:164ac155109226b3a2606ee6dda899ccfbe6e7e18b5bdc3fbc00f79cc074157d"}, + {file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6b1225024cf0ef5d15934b5ffe9baf860fe8bc68a796513f5ea4f5056de30bca"}, + {file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d6546e8073dc382e60fcae4a001a5a1bc46da5eab4a4878acc2d12072d6166d5"}, + {file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f1d2942605c894162252d6259b0121bf1cb493071a1ea8cb35d79cb3e6ac5bc"}, + {file = "orjson-3.10.14-cp310-cp310-win32.whl", hash = "sha256:397083806abd51cf2b3bbbf6c347575374d160331a2d33c5823e22249ad3118b"}, + {file = "orjson-3.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:fa18f949d3183a8d468367056be989666ac2bef3a72eece0bade9cdb733b3c28"}, + {file = "orjson-3.10.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f506fd666dd1ecd15a832bebc66c4df45c1902fd47526292836c339f7ba665a9"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe5fd254cfb0eeee13b8ef7ecb20f5d5a56ddda8a587f3852ab2cedfefdb5f6"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ddc8c866d7467f5ee2991397d2ea94bcf60d0048bdd8ca555740b56f9042725"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af8e42ae4363773658b8d578d56dedffb4f05ceeb4d1d4dd3fb504950b45526"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84dd83110503bc10e94322bf3ffab8bc49150176b49b4984dc1cce4c0a993bf9"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f5bfc0399cd4811bf10ec7a759c7ab0cd18080956af8ee138097d5b5296a95"}, + {file = "orjson-3.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868943660fb2a1e6b6b965b74430c16a79320b665b28dd4511d15ad5038d37d5"}, + {file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33449c67195969b1a677533dee9d76e006001213a24501333624623e13c7cc8e"}, + {file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4c9f60f9fb0b5be66e416dcd8c9d94c3eabff3801d875bdb1f8ffc12cf86905"}, + {file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0de4d6315cfdbd9ec803b945c23b3a68207fd47cbe43626036d97e8e9561a436"}, + {file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83adda3db595cb1a7e2237029b3249c85afbe5c747d26b41b802e7482cb3933e"}, + {file = "orjson-3.10.14-cp311-cp311-win32.whl", hash = "sha256:998019ef74a4997a9d741b1473533cdb8faa31373afc9849b35129b4b8ec048d"}, + {file = "orjson-3.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:9d034abdd36f0f0f2240f91492684e5043d46f290525d1117712d5b8137784eb"}, + {file = "orjson-3.10.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2ad4b7e367efba6dc3f119c9a0fcd41908b7ec0399a696f3cdea7ec477441b09"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f496286fc85e93ce0f71cc84fc1c42de2decf1bf494094e188e27a53694777a7"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7f189bbfcded40e41a6969c1068ba305850ba016665be71a217918931416fbf"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cc8204f0b75606869c707da331058ddf085de29558b516fc43c73ee5ee2aadb"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deaa2899dff7f03ab667e2ec25842d233e2a6a9e333efa484dfe666403f3501c"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1c3ea52642c9714dc6e56de8a451a066f6d2707d273e07fe8a9cc1ba073813d"}, + {file = "orjson-3.10.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d3f9ed72e7458ded9a1fb1b4d4ed4c4fdbaf82030ce3f9274b4dc1bff7ace2b"}, + {file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:07520685d408a2aba514c17ccc16199ff2934f9f9e28501e676c557f454a37fe"}, + {file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:76344269b550ea01488d19a2a369ab572c1ac4449a72e9f6ac0d70eb1cbfb953"}, + {file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e2979d0f2959990620f7e62da6cd954e4620ee815539bc57a8ae46e2dacf90e3"}, + {file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03f61ca3674555adcb1aa717b9fc87ae936aa7a63f6aba90a474a88701278780"}, + {file = "orjson-3.10.14-cp312-cp312-win32.whl", hash = "sha256:d5075c54edf1d6ad81d4c6523ce54a748ba1208b542e54b97d8a882ecd810fd1"}, + {file = "orjson-3.10.14-cp312-cp312-win_amd64.whl", hash = "sha256:175cafd322e458603e8ce73510a068d16b6e6f389c13f69bf16de0e843d7d406"}, + {file = "orjson-3.10.14-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0905ca08a10f7e0e0c97d11359609300eb1437490a7f32bbaa349de757e2e0c7"}, + {file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92d13292249f9f2a3e418cbc307a9fbbef043c65f4bd8ba1eb620bc2aaba3d15"}, + {file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90937664e776ad316d64251e2fa2ad69265e4443067668e4727074fe39676414"}, + {file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ed3d26c4cb4f6babaf791aa46a029265850e80ec2a566581f5c2ee1a14df4f1"}, + {file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:56ee546c2bbe9599aba78169f99d1dc33301853e897dbaf642d654248280dc6e"}, + {file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:901e826cb2f1bdc1fcef3ef59adf0c451e8f7c0b5deb26c1a933fb66fb505eae"}, + {file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26336c0d4b2d44636e1e1e6ed1002f03c6aae4a8a9329561c8883f135e9ff010"}, + {file = "orjson-3.10.14-cp313-cp313-win32.whl", hash = "sha256:e2bc525e335a8545c4e48f84dd0328bc46158c9aaeb8a1c2276546e94540ea3d"}, + {file = "orjson-3.10.14-cp313-cp313-win_amd64.whl", hash = "sha256:eca04dfd792cedad53dc9a917da1a522486255360cb4e77619343a20d9f35364"}, + {file = "orjson-3.10.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a0fba3b8a587a54c18585f077dcab6dd251c170d85cfa4d063d5746cd595a0f"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175abf3d20e737fec47261d278f95031736a49d7832a09ab684026528c4d96db"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29ca1a93e035d570e8b791b6c0feddd403c6a5388bfe870bf2aa6bba1b9d9b8e"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f77202c80e8ab5a1d1e9faf642343bee5aaf332061e1ada4e9147dbd9eb00c46"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2ec73b7099b6a29b40a62e08a23b936423bd35529f8f55c42e27acccde7954"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d1679df9f9cd9504f8dff24555c1eaabba8aad7f5914f28dab99e3c2552c9d"}, + {file = "orjson-3.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691ab9a13834310a263664313e4f747ceb93662d14a8bdf20eb97d27ed488f16"}, + {file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b11ed82054fce82fb74cea33247d825d05ad6a4015ecfc02af5fbce442fbf361"}, + {file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:e70a1d62b8288677d48f3bea66c21586a5f999c64ecd3878edb7393e8d1b548d"}, + {file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:16642f10c1ca5611251bd835de9914a4b03095e28a34c8ba6a5500b5074338bd"}, + {file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3871bad546aa66c155e3f36f99c459780c2a392d502a64e23fb96d9abf338511"}, + {file = "orjson-3.10.14-cp38-cp38-win32.whl", hash = "sha256:0293a88815e9bb5c90af4045f81ed364d982f955d12052d989d844d6c4e50945"}, + {file = "orjson-3.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:6169d3868b190d6b21adc8e61f64e3db30f50559dfbdef34a1cd6c738d409dfc"}, + {file = "orjson-3.10.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:06d4ec218b1ec1467d8d64da4e123b4794c781b536203c309ca0f52819a16c03"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962c2ec0dcaf22b76dee9831fdf0c4a33d4bf9a257a2bc5d4adc00d5c8ad9034"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21d3be4132f71ef1360385770474f29ea1538a242eef72ac4934fe142800e37f"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28ed60597c149a9e3f5ad6dd9cebaee6fb2f0e3f2d159a4a2b9b862d4748860"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e947f70167fe18469f2023644e91ab3d24f9aed69a5e1c78e2c81b9cea553fb"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64410696c97a35af2432dea7bdc4ce32416458159430ef1b4beb79fd30093ad6"}, + {file = "orjson-3.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8050a5d81c022561ee29cd2739de5b4445f3c72f39423fde80a63299c1892c52"}, + {file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b49a28e30d3eca86db3fe6f9b7f4152fcacbb4a467953cd1b42b94b479b77956"}, + {file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ca041ad20291a65d853a9523744eebc3f5a4b2f7634e99f8fe88320695ddf766"}, + {file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d313a2998b74bb26e9e371851a173a9b9474764916f1fc7971095699b3c6e964"}, + {file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7796692136a67b3e301ef9052bde6fe8e7bd5200da766811a3a608ffa62aaff0"}, + {file = "orjson-3.10.14-cp39-cp39-win32.whl", hash = "sha256:eee4bc767f348fba485ed9dc576ca58b0a9eac237f0e160f7a59bce628ed06b3"}, + {file = "orjson-3.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:96a1c0ee30fb113b3ae3c748fd75ca74a157ff4c58476c47db4d61518962a011"}, + {file = "orjson-3.10.14.tar.gz", hash = "sha256:cf31f6f071a6b8e7aa1ead1fa27b935b48d00fbfa6a28ce856cfff2d5dd68eed"}, ] [[package]] @@ -2624,13 +2630,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.7.0" +version = "2.7.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"}, - {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"}, + {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, + {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, ] [package.dependencies] @@ -2706,13 +2712,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.25.0" +version = "0.25.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, - {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, ] [package.dependencies] @@ -3043,29 +3049,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.3" +version = "0.9.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, - {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, - {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, - {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, - {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, - {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, - {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, - {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, - {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, + {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, + {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, + {file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"}, + {file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"}, + {file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"}, + {file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"}, + {file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"}, ] [[package]] From 8b4390c247fbd410cc71867a95fd5d7a17241aec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:06:18 -0600 Subject: [PATCH 013/184] chore(deps): update dependency @types/node to ^22.10.5 (#15246) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 10 +++++----- cli/package.json | 2 +- e2e/package-lock.json | 12 ++++++------ e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 8 ++++---- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 8 ++++---- server/package.json | 2 +- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 7bd6343936abc..41077bb27bcd3 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "typescript": "^5.3.3" } }, @@ -1397,9 +1397,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/cli/package.json b/cli/package.json index ddbdcec0a202f..d1bad600ac502 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d3d62ae386fe5..233124238ad5b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "typescript": "^5.3.3" } }, @@ -1658,9 +1658,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 54224211246be..b31c18bbddf36 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index bd3862349f20e..ddcc46658a1e5 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 51fd621e72758..5f9603554c36b 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index 41fb79d5f05e1..ef3621de6a8f3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -86,7 +86,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", @@ -5128,9 +5128,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" diff --git a/server/package.json b/server/package.json index b9fa78a3236a1..14b4e2fa320b8 100644 --- a/server/package.json +++ b/server/package.json @@ -111,7 +111,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", From f3dbbfa16d53cbe15405fc3442d479dbde3e41ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:06:30 -0600 Subject: [PATCH 014/184] chore(deps): update redis:6.2-alpine docker digest to 905c4ee (#15245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.dev.yml | 2 +- docker/docker-compose.prod.yml | 2 +- e2e/docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 5da5bd3f913e9..fc1e2602daf1c 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -106,7 +106,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8 + image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 85213900790d9..1728488c2f6a2 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -47,7 +47,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8 + image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index d9117b1b4aef3..c925f6b3d07bd 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -34,7 +34,7 @@ services: - 2285:2285 redis: - image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8 + image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 From ca75bba3b03aa313d4b57b106dbae713a333440a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:06:45 -0600 Subject: [PATCH 015/184] chore(deps): update prom/prometheus docker digest to 6559acb (#15244) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 1728488c2f6a2..d0209ed285957 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -91,7 +91,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd + image: prom/prometheus@sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From a39fbcb8ac2ab3c6c48bbaea1e2fafb568affe51 Mon Sep 17 00:00:00 2001 From: imakida Date: Sat, 11 Jan 2025 18:08:08 -1000 Subject: [PATCH 016/184] feat: #15237 toggle password visibility on shared albums (#15238) * feat: toggle password visibility on shared albums * feat: toggle password visibility on shared albums * use password-field component * remove div wrapping PasswordField --------- Co-authored-by: Ian --- e2e/src/web/specs/shared-link.e2e-spec.ts | 34 ++++++++++++++++++- .../[[assetId=id]]/+page.svelte | 5 +-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index 2a02e429a5900..ed81db4ef58db 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -6,7 +6,7 @@ import { SharedLinkType, createAlbum, } from '@immich/sdk'; -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { asBearerAuth, utils } from 'src/utils'; test.describe('Shared Links', () => { @@ -65,6 +65,38 @@ test.describe('Shared Links', () => { await page.getByRole('heading', { name: 'Test Album' }).waitFor(); }); + test('show-password button visible', async ({ page }) => { + await page.goto(`/share/${sharedLinkPassword.key}`); + await page.getByPlaceholder('Password').fill('test-password'); + await page.getByRole('button', { name: 'Show password' }).waitFor(); + }); + + test('view password for shared link', async ({ page }) => { + await page.goto(`/share/${sharedLinkPassword.key}`); + const input = page.getByPlaceholder('Password'); + await input.fill('test-password'); + await page.getByRole('button', { name: 'Show password' }).click(); + // await page.getByText('test-password', { exact: true }).waitFor(); + await expect(input).toHaveAttribute('type', 'text'); + }); + + test('hide-password button visible', async ({ page }) => { + await page.goto(`/share/${sharedLinkPassword.key}`); + const input = page.getByPlaceholder('Password'); + await input.fill('test-password'); + await page.getByRole('button', { name: 'Show password' }).click(); + await page.getByRole('button', { name: 'Hide password' }).waitFor(); + }); + + test('hide password for shared link', async ({ page }) => { + await page.goto(`/share/${sharedLinkPassword.key}`); + const input = page.getByPlaceholder('Password'); + await input.fill('test-password'); + await page.getByRole('button', { name: 'Show password' }).click(); + await page.getByRole('button', { name: 'Hide password' }).click(); + await expect(input).toHaveAttribute('type', 'password'); + }); + test('show error for invalid shared link', async ({ page }) => { await page.goto('/share/invalid'); await page.getByRole('heading', { name: 'Invalid share key' }).waitFor(); diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte index dfe465f94defd..83dc40598aabb 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -5,6 +5,7 @@ import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import ThemeButton from '$lib/components/shared-components/theme-button.svelte'; + import PasswordField from '$lib/components/shared-components/password-field.svelte'; import { user } from '$lib/stores/user.store'; import { handleError } from '$lib/utils/handle-error'; import { getMySharedLink, SharedLinkType } from '@immich/sdk'; @@ -80,8 +81,8 @@ {$t('sharing_enter_password')}
-
- + +
From fef36e6a3732879c958a341268dbddf63af5b2a4 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:09:19 -0500 Subject: [PATCH 017/184] chore(server)!: default max bitrate unit to kbps (#15264) default unit to kbps --- server/src/services/media.service.spec.ts | 16 ++++++++++++++++ server/src/utils/media.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 36a9045677460..f76f832cf354b 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1138,6 +1138,22 @@ describe(MediaService.name, () => { ); }); + it('should default max bitrate to kbps if no unit is provided', async () => { + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']), + twoPass: false, + }), + ); + }); + it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } }); diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 678e8cb15a48e..cd8eedba258b0 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -272,7 +272,7 @@ export class BaseConfig implements VideoCodecSWConfig { getBitrateUnit() { const maxBitrate = this.getMaxBitrateValue(); - return this.config.maxBitrate.trim().slice(maxBitrate.toString().length); // use inputted unit if provided + return this.config.maxBitrate.trim().slice(maxBitrate.toString().length) || 'k'; // use inputted unit if provided, else default to kbps } getMaxBitrateValue() { From be1187bc4639ff87f879bb03e696ad5c274f9ab5 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:09:54 -0500 Subject: [PATCH 018/184] chore(docs): clarify experimental network features (#15228) * auth * URL switch * mobile app * caps * headers, app changes * oxford comma * Match case to other use in Immich * add url * asset download also causes issues --- docs/docs/FAQ.mdx | 10 +++++++--- mobile/assets/i18n/en-US.json | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 755698ba1d9fc..5c6bbe32988a8 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -53,10 +53,14 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca - Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich. - Use the Immich app more often. -### Why are features not working with a self-signed cert or mTLS? +### Why are features in the mobile app not working with a self-signed certificate, Basic Auth, custom headers, or mutual TLS? -Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background). -We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/). +These network features are experimental. They often do not work with video playback, asset upload or download, and other features. +Many of these limitations are tracked in [#15230](https://github.com/immich-app/immich/issues/15230). +Instead of these experimental features, we recommend using the URL switching feature, a VPN, or a [free trusted SSL certificate](https://letsencrypt.org/) for your domain. + +We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them. +Please discuss any large PRs with our dev team to ensure your time is not wasted. --- diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 1ff40b3566e00..c0d23c681dfb2 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -16,7 +16,7 @@ "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates (EXPERIMENTAL)", "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_tile_title": "Advanced", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", @@ -174,7 +174,7 @@ "client_cert_remove": "Remove", "client_cert_remove_msg": "Client certificate is removed", "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_title": "SSL Client Certificate (EXPERIMENTAL)", "common_add_to_album": "Add to album", "common_change_password": "Change Password", "common_create_new_album": "Create new album", @@ -278,7 +278,7 @@ "header_settings_header_value_input": "Header value", "header_settings_page_title": "Proxy Headers", "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "headers_settings_tile_title": "Custom proxy headers (EXPERIMENTAL)", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", @@ -658,4 +658,4 @@ "viewer_unstack": "Un-Stack", "wifi_name": "WiFi Name", "your_wifi_name": "Your WiFi name" -} \ No newline at end of file +} From e7abfe3067ef199f8650ec4f34d37c8a69a392cf Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sun, 12 Jan 2025 05:10:23 +0100 Subject: [PATCH 019/184] docs: clarify filesystem backup paths (#15243) * docs: clarify filesystem backup paths * fix: backticks --- docs/docs/administration/backup-and-restore.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 9fffdf103b1c2..1b1775018efe3 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -95,12 +95,14 @@ Some deployment methods make it difficult to start the database without also sta ## Filesystem -Immich stores two types of content in the filesystem: (1) original, unmodified assets (photos and videos), and (2) generated content. Only the original content needs to be backed-up, which is stored in the following folders: +Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders: 1. `UPLOAD_LOCATION/library` 2. `UPLOAD_LOCATION/upload` 3. `UPLOAD_LOCATION/profile` +If you choose to back up only those folders, you will need to rerun the transcoding and thumbnail generation jobs for all assets after you restore from a backup. + :::caution If you moved some of these folders onto a different storage device, such as `profile/`, make sure to adjust the backup path to match your setup ::: From 77d4eb8787492b126c1e0ae1cbb609a55f490957 Mon Sep 17 00:00:00 2001 From: Ferdinand Holzer Date: Sun, 12 Jan 2025 05:10:33 +0100 Subject: [PATCH 020/184] fix(web): render whitespaces in file names and paths on photos and folders pages correctly (#15266) --- web/src/lib/components/asset-viewer/detail-panel.svelte | 4 ++-- .../shared-components/gallery-viewer/gallery-viewer.svelte | 2 +- .../components/shared-components/tree/breadcrumbs.svelte | 7 +++++-- .../shared-components/tree/tree-item-thumbnails.svelte | 4 +++- web/src/lib/components/shared-components/tree/tree.svelte | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 11425067f5066..fde4efae95fa4 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -364,7 +364,7 @@
-

+

{asset.originalFileName} {#if isOwner} - + {asset.originalPath}

diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 8f8a067a902ba..65c6c20e7b75b 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -354,7 +354,7 @@ /> {#if showAssetName}
{asset.originalFileName}
diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte index 1d841339bc0a5..c4ea6356872fd 100644 --- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -52,9 +52,12 @@ > {#if isLastSegment} -

{segment}

+

{segment}

{:else} - + {segment} {/if} diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte index b84f25060eeab..36ef781656917 100644 --- a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte +++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte @@ -22,7 +22,9 @@ type="button" > -

{item}

+

+ {item} +

{/each}
diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index c6a13ec197e80..ccc4181abea56 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -47,7 +47,7 @@ size={20} /> - {value} + {value} {#if isOpen} From abf5b0afe1092d9860b564fda6a471d13ae6207f Mon Sep 17 00:00:00 2001 From: Jin Xuan <87897838+jinxuan-owyong@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:28:39 +0800 Subject: [PATCH 021/184] fix(web): mismatched deviceAssetId when uploading images (#15130) Co-authored-by: Alex --- web/src/lib/utils/file-uploader.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 2e31605e91605..407501e6229f1 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -83,14 +83,19 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { }); }; -export const fileUploadHandler = async (files: File[], albumId?: string, assetId?: string): Promise => { +export const fileUploadHandler = async ( + files: File[], + albumId?: string, + replaceAssetId?: string, +): Promise => { const extensions = await getExtensions(); const promises = []; for (const file of files) { const name = file.name.toLowerCase(); if (extensions.some((extension) => name.endsWith(extension))) { - uploadAssetsStore.addItem({ id: getDeviceAssetId(file), file, albumId }); - promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId, assetId))); + const deviceAssetId = getDeviceAssetId(file); + uploadAssetsStore.addItem({ id: deviceAssetId, file, albumId }); + promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, deviceAssetId, albumId, replaceAssetId))); } } @@ -103,9 +108,13 @@ function getDeviceAssetId(asset: File) { } // TODO: should probably use the @api SDK -async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: string): Promise { +async function fileUploader( + assetFile: File, + deviceAssetId: string, + albumId?: string, + replaceAssetId?: string, +): Promise { const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); - const deviceAssetId = getDeviceAssetId(assetFile); const $t = get(t); uploadAssetsStore.markStarted(deviceAssetId); @@ -148,6 +157,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: } } catch (error) { console.error(`Error calculating sha1 file=${assetFile.name})`, error); + throw error; } } From c4a8fdf0f3a84fbeeb5b8aedfff0bf51f16e327b Mon Sep 17 00:00:00 2001 From: Desmond Cox Date: Sun, 12 Jan 2025 16:44:51 +0100 Subject: [PATCH 022/184] fix(cli): handle folders with single quotes (#15283) * fix(cli): handle folders with single quotes * fix(cli): skip single quote test on Windows * fix(cli): support double quote and backtick as well --- cli/src/utils.spec.ts | 21 +++++++++++++++++++-- cli/src/utils.ts | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cli/src/utils.spec.ts b/cli/src/utils.spec.ts index 3e7e55fcb69e1..93f031872be7e 100644 --- a/cli/src/utils.spec.ts +++ b/cli/src/utils.spec.ts @@ -6,6 +6,7 @@ interface Test { test: string; options: Omit; files: Record; + skipOnWin32?: boolean; } const cwd = process.cwd(); @@ -48,6 +49,18 @@ const tests: Test[] = [ '/photos/image.jpg': true, }, }, + { + test: 'should crawl folders with quotes', + options: { + pathsToCrawl: ["/photo's/", '/photo"s/', '/photo`s/'], + }, + files: { + "/photo's/image1.jpg": true, + '/photo"s/image2.jpg': true, + '/photo`s/image3.jpg': true, + }, + skipOnWin32: true, // single quote interferes with mockfs root on Windows + }, { test: 'should crawl a single file', options: { @@ -270,8 +283,12 @@ describe('crawl', () => { }); describe('crawl', () => { - for (const { test, options, files } of tests) { - it(test, async () => { + for (const { test: name, options, files, skipOnWin32 } of tests) { + if (process.platform === 'win32' && skipOnWin32) { + test.skip(name); + continue; + } + it(name, async () => { // The file contents is the same as the path. mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file]))); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 7bbbb5615b640..27cc2f9e082a7 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -146,7 +146,7 @@ export const crawl = async (options: CrawlOptions): Promise => { } const searchPatterns = patterns.map((pattern) => { - let escapedPattern = pattern; + let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]'); if (recursive) { escapedPattern = escapedPattern + '/**'; } From efe4396e5486c6d30fb2c808248fc52b47b8ecde Mon Sep 17 00:00:00 2001 From: Austin Dudzik <65981261+austin-dudzik@users.noreply.github.com> Date: Sun, 12 Jan 2025 09:51:55 -0600 Subject: [PATCH 023/184] fix(docs): Fix link label to refer to correct location on page (#15279) Fix link label to refer to correct location on page --- docs/docs/FAQ.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 5c6bbe32988a8..f82bc74f5ffa9 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -282,7 +282,7 @@ The initial backup is the most intensive due to the number of jobs running. The - For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this. - At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further. - It's recommended to only apply these constraints _after_ taking some of the measures here for best performance. -- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. +- If these changes are not enough, see [above](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. ### Can I limit CPU and RAM usage? From a6c8eb57f1db07fc493007eea6a2c1d8983a1ffb Mon Sep 17 00:00:00 2001 From: Dr-Electron Date: Mon, 13 Jan 2025 04:12:26 +0100 Subject: [PATCH 024/184] fix(docs): fix admonition in mobile section (#15291) fix(docs): Fix admonition in mobile section --- docs/docs/features/mobile-app.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 423e66d9eeab1..38222479b01d9 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -36,11 +36,15 @@ You can enable automatic backup on supported devices. For more information see [ If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server. First, you need to enable the Storage Indicator in your app's settings. Navigate to **Settings -> Photo Grid** and enable **"Show Storage indicator on asset tiles"**; this makes it easy to distinguish local-only assets and synced assets. + :::note + This will enable a small cloud icon on the bottom right corner of the asset tile, indicating that the asset is synced to the server: 1. - Local-only asset; not synced to the server -2. - Asset is synced to the server ::: +2. - Asset is synced to the server + +::: Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **Library -> On this device**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu. From 3da750117f49c10685d27a8b69d180f052980adf Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jan 2025 19:30:34 -0600 Subject: [PATCH 025/184] refactor: migrate user repository to kysely (#15296) * refactor: migrate user repository to kysely * refactor: migrate user repository to kysely * refactor: migrate user repository to kysely * refactor: migrate user repository to kysely * fix: test * clean up * fix: metadata retrieval bug * use correct typeing for upsert metadata * pr feedback * pr feedback * fix: add deletedAt check * fix: get non deleted user by default * remove console.log * fix: stop kysely after command finishes * final clean up --------- Co-authored-by: Jason Rasmussen --- server/src/app.module.ts | 9 +- server/src/decorators.ts | 1 + server/src/entities/user.entity.ts | 9 + server/src/interfaces/database.interface.ts | 1 + server/src/interfaces/user.interface.ts | 16 +- server/src/queries/user.repository.sql | 365 ++++++++++-------- .../src/repositories/database.repository.ts | 8 +- server/src/repositories/user.repository.ts | 299 ++++++++------ server/src/services/album.service.spec.ts | 4 +- server/src/services/auth.service.spec.ts | 24 +- server/src/services/auth.service.ts | 2 +- server/src/services/base.service.ts | 6 +- server/src/services/cli.service.spec.ts | 2 +- server/src/services/cli.service.ts | 4 + .../src/services/user-admin.service.spec.ts | 14 +- server/src/services/user.service.spec.ts | 8 +- .../repositories/database.repository.mock.ts | 1 + web/package-lock.json | 2 +- 18 files changed, 455 insertions(+), 320 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 9d96a0499ba26..d0422756b6e31 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -24,6 +24,7 @@ import { repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { teardownTelemetry } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; +import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; const common = [...services, ...repositories]; @@ -106,4 +107,10 @@ export class MicroservicesModule extends BaseModule {} imports: [...imports], providers: [...common, ...commands, SchedulerRegistry], }) -export class ImmichAdminModule {} +export class ImmichAdminModule implements OnModuleDestroy { + constructor(private service: CliService) {} + + async onModuleDestroy() { + await this.service.cleanup(); + } +} diff --git a/server/src/decorators.ts b/server/src/decorators.ts index c2bbe19b28fd9..047b9ec4a7079 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -99,6 +99,7 @@ export const DummyValue = { BUFFER: Buffer.from('abcdefghi'), DATE: new Date(), TIME_BUCKET: '2024-01-01T00:00:00.000Z', + BOOLEAN: true, }; export const GENERATE_SQL_KEY = 'generate-sql-key'; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index ea446be390844..3f5b470ce467f 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -1,3 +1,6 @@ +import { ExpressionBuilder } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { DB } from 'src/db'; import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; @@ -71,3 +74,9 @@ export class UserEntity { @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) profileChangedAt!: Date; } + +export const withMetadata = (eb: ExpressionBuilder) => { + return jsonArrayFrom( + eb.selectFrom('user_metadata').selectAll('user_metadata').whereRef('users.id', '=', 'user_metadata.userId'), + ).as('metadata'); +}; diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index 5ad37efa71ee5..8cfc040271433 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -61,6 +61,7 @@ export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { init(): void; reconnect(): Promise; + shutdown(): Promise; getExtensionVersion(extension: DatabaseExtension): Promise; getExtensionVersionRange(extension: VectorExtension): string; getPostgresVersion(): Promise; diff --git a/server/src/interfaces/user.interface.ts b/server/src/interfaces/user.interface.ts index 385a4d3d50e91..6ff3fc824aabe 100644 --- a/server/src/interfaces/user.interface.ts +++ b/server/src/interfaces/user.interface.ts @@ -1,3 +1,5 @@ +import { Insertable, Updateable } from 'kysely'; +import { Users } from 'src/db'; import { UserMetadata } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; @@ -23,17 +25,17 @@ export interface UserFindOptions { export const IUserRepository = 'IUserRepository'; export interface IUserRepository { - get(id: string, options: UserFindOptions): Promise; - getAdmin(): Promise; + get(id: string, options: UserFindOptions): Promise; + getAdmin(): Promise; hasAdmin(): Promise; - getByEmail(email: string, withPassword?: boolean): Promise; - getByStorageLabel(storageLabel: string): Promise; - getByOAuthId(oauthId: string): Promise; + getByEmail(email: string, withPassword?: boolean): Promise; + getByStorageLabel(storageLabel: string): Promise; + getByOAuthId(oauthId: string): Promise; getDeletedUsers(): Promise; getList(filter?: UserListFilter): Promise; getUserStats(): Promise; - create(user: Partial): Promise; - update(id: string, user: Partial): Promise; + create(user: Insertable): Promise; + update(id: string, user: Updateable): Promise; upsertMetadata(id: string, item: { key: T; value: UserMetadata[T] }): Promise; deleteMetadata(id: string, key: T): Promise; delete(user: UserEntity, hard?: boolean): Promise; diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index c35dc540cef42..7ae8003a0962b 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -1,195 +1,222 @@ -- NOTE: This file is auto generated by ./sql-generator +-- UserRepository.get +select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "user_metadata".* + from + "user_metadata" + where + "users"."id" = "user_metadata"."userId" + ) as agg + ) as "metadata" +from + "users" +where + "users"."id" = $1 + and "users"."deletedAt" is null + -- UserRepository.getAdmin -SELECT - "UserEntity"."id" AS "UserEntity_id", - "UserEntity"."name" AS "UserEntity_name", - "UserEntity"."isAdmin" AS "UserEntity_isAdmin", - "UserEntity"."email" AS "UserEntity_email", - "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."oauthId" AS "UserEntity_oauthId", - "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", - "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", - "UserEntity"."createdAt" AS "UserEntity_createdAt", - "UserEntity"."deletedAt" AS "UserEntity_deletedAt", - "UserEntity"."status" AS "UserEntity_status", - "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", - "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" -FROM - "users" "UserEntity" -WHERE - ((("UserEntity"."isAdmin" = $1))) - AND ("UserEntity"."deletedAt" IS NULL) -LIMIT - 1 +select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" +from + "users" +where + "users"."isAdmin" = $1 + and "users"."deletedAt" is null -- UserRepository.hasAdmin -SELECT - 1 AS "row_exists" -FROM - ( - SELECT - 1 AS dummy_column - ) "dummy_table" -WHERE - EXISTS ( - SELECT - 1 - FROM - "users" "UserEntity" - WHERE - ((("UserEntity"."isAdmin" = $1))) - AND ("UserEntity"."deletedAt" IS NULL) - ) -LIMIT - 1 +select + "users"."id" +from + "users" +where + "users"."isAdmin" = $1 + and "users"."deletedAt" is null -- UserRepository.getByEmail -SELECT - "user"."id" AS "user_id", - "user"."name" AS "user_name", - "user"."isAdmin" AS "user_isAdmin", - "user"."email" AS "user_email", - "user"."storageLabel" AS "user_storageLabel", - "user"."oauthId" AS "user_oauthId", - "user"."profileImagePath" AS "user_profileImagePath", - "user"."shouldChangePassword" AS "user_shouldChangePassword", - "user"."createdAt" AS "user_createdAt", - "user"."deletedAt" AS "user_deletedAt", - "user"."status" AS "user_status", - "user"."updatedAt" AS "user_updatedAt", - "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", - "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes", - "user"."profileChangedAt" AS "user_profileChangedAt" -FROM - "users" "user" -WHERE - ("user"."email" = $1) - AND ("user"."deletedAt" IS NULL) +select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" +from + "users" +where + "email" = $1 + and "users"."deletedAt" is null -- UserRepository.getByStorageLabel -SELECT - "UserEntity"."id" AS "UserEntity_id", - "UserEntity"."name" AS "UserEntity_name", - "UserEntity"."isAdmin" AS "UserEntity_isAdmin", - "UserEntity"."email" AS "UserEntity_email", - "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."oauthId" AS "UserEntity_oauthId", - "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", - "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", - "UserEntity"."createdAt" AS "UserEntity_createdAt", - "UserEntity"."deletedAt" AS "UserEntity_deletedAt", - "UserEntity"."status" AS "UserEntity_status", - "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", - "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" -FROM - "users" "UserEntity" -WHERE - ((("UserEntity"."storageLabel" = $1))) - AND ("UserEntity"."deletedAt" IS NULL) -LIMIT - 1 +select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" +from + "users" +where + "users"."storageLabel" = $1 + and "users"."deletedAt" is null -- UserRepository.getByOAuthId -SELECT - "UserEntity"."id" AS "UserEntity_id", - "UserEntity"."name" AS "UserEntity_name", - "UserEntity"."isAdmin" AS "UserEntity_isAdmin", - "UserEntity"."email" AS "UserEntity_email", - "UserEntity"."storageLabel" AS "UserEntity_storageLabel", - "UserEntity"."oauthId" AS "UserEntity_oauthId", - "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", - "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", - "UserEntity"."createdAt" AS "UserEntity_createdAt", - "UserEntity"."deletedAt" AS "UserEntity_deletedAt", - "UserEntity"."status" AS "UserEntity_status", - "UserEntity"."updatedAt" AS "UserEntity_updatedAt", - "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", - "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" -FROM - "users" "UserEntity" -WHERE - ((("UserEntity"."oauthId" = $1))) - AND ("UserEntity"."deletedAt" IS NULL) -LIMIT - 1 +select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" +from + "users" +where + "users"."oauthId" = $1 + and "users"."deletedAt" is null -- UserRepository.getUserStats -SELECT - "users"."id" AS "userId", - "users"."name" AS "userName", - "users"."quotaSizeInBytes" AS "quotaSizeInBytes", - COUNT("assets"."id") FILTER ( - WHERE - "assets"."type" = 'IMAGE' - AND "assets"."isVisible" - ) AS "photos", - COUNT("assets"."id") FILTER ( - WHERE - "assets"."type" = 'VIDEO' - AND "assets"."isVisible" - ) AS "videos", - COALESCE( - SUM("exif"."fileSizeInByte") FILTER ( - WHERE - "assets"."libraryId" IS NULL +select + "users"."id" as "userId", + "users"."name" as "userName", + "users"."quotaSizeInBytes" as "quotaSizeInBytes", + count(*) filter ( + where + ( + "assets"."type" = $1 + and "assets"."isVisible" = $2 + ) + ) as "photos", + count(*) filter ( + where + ( + "assets"."type" = $3 + and "assets"."isVisible" = $4 + ) + ) as "videos", + coalesce( + sum("exif"."fileSizeInByte") filter ( + where + "assets"."libraryId" is null ), 0 - ) AS "usage", - COALESCE( - SUM("exif"."fileSizeInByte") FILTER ( - WHERE - "assets"."libraryId" IS NULL - AND "assets"."type" = 'IMAGE' + ) as "usage", + coalesce( + sum("exif"."fileSizeInByte") filter ( + where + ( + "assets"."libraryId" is null + and "assets"."type" = $5 + ) ), 0 - ) AS "usagePhotos", - COALESCE( - SUM("exif"."fileSizeInByte") FILTER ( - WHERE - "assets"."libraryId" IS NULL - AND "assets"."type" = 'VIDEO' + ) as "usagePhotos", + coalesce( + sum("exif"."fileSizeInByte") filter ( + where + ( + "assets"."libraryId" is null + and "assets"."type" = $6 + ) ), 0 - ) AS "usageVideos" -FROM - "users" "users" - LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id" - AND ("assets"."deletedAt" IS NULL) - LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" -WHERE - "users"."deletedAt" IS NULL -GROUP BY + ) as "usageVideos" +from + "users" + left join "assets" on "assets"."ownerId" = "users"."id" + left join "exif" on "exif"."assetId" = "assets"."id" +where + "assets"."deletedAt" is null +group by "users"."id" -ORDER BY - "users"."createdAt" ASC +order by + "users"."createdAt" asc -- UserRepository.updateUsage -UPDATE "users" -SET - "quotaUsageInBytes" = "quotaUsageInBytes" + 50, - "updatedAt" = CURRENT_TIMESTAMP -WHERE - "id" = $1 +update "users" +set + "quotaUsageInBytes" = "quotaUsageInBytes" + $1, + "updatedAt" = $2 +where + "id" = $3::uuid + and "users"."deletedAt" is null -- UserRepository.syncUsage -UPDATE "users" -SET +update "users" +set "quotaUsageInBytes" = ( - SELECT - COALESCE(SUM(exif."fileSizeInByte"), 0) - FROM - "assets" "assets" - LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" - WHERE - "assets"."ownerId" = users.id - AND "assets"."libraryId" IS NULL + select + coalesce(sum("exif"."fileSizeInByte"), 0) as "usage" + from + "assets" + left join "exif" on "exif"."assetId" = "assets"."id" + where + "assets"."libraryId" is null + and "assets"."ownerId" = "users"."id" ), - "updatedAt" = CURRENT_TIMESTAMP -WHERE - users.id = $1 + "updatedAt" = $1 +where + "users"."deletedAt" is null + and "users"."id" = $2::uuid diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 0eefce0cd24e9..7188678212276 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,7 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import AsyncLock from 'async-lock'; -import { sql } from 'kysely'; +import { Kysely, sql } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; import semver from 'semver'; import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { DB } from 'src/db'; @@ -27,6 +28,7 @@ export class DatabaseRepository implements IDatabaseRepository { private readonly asyncLock = new AsyncLock(); constructor( + @InjectKysely() private db: Kysely, @InjectDataSource() private dataSource: DataSource, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IConfigRepository) configRepository: IConfigRepository, @@ -35,6 +37,10 @@ export class DatabaseRepository implements IDatabaseRepository { this.logger.setContext(DatabaseRepository.name); } + async shutdown() { + await this.db.destroy(); + } + init() { for (const metadata of this.dataSource.entityMetadatas) { const table = metadata.tableName as keyof DB; diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index a2e4375701a2a..e7c65b3f01775 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -1,127 +1,212 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { UserMetadata, UserMetadataEntity } from 'src/entities/user-metadata.entity'; -import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadata } from 'src/entities/user-metadata.entity'; +import { UserEntity, withMetadata } from 'src/entities/user.entity'; import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryResponse, } from 'src/interfaces/user.interface'; -import { IsNull, Not, Repository } from 'typeorm'; +import { asUuid } from 'src/utils/database'; + +const columns = [ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', +] as const; + +type Upsert = Insertable; @Injectable() export class UserRepository implements IUserRepository { - constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(UserEntity) private userRepository: Repository, - @InjectRepository(UserMetadataEntity) private metadataRepository: Repository, - ) {} + constructor(@InjectKysely() private db: Kysely) {} - async get(userId: string, options: UserFindOptions): Promise { + @GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] }) + get(userId: string, options: UserFindOptions): Promise { options = options || {}; - return this.userRepository.findOne({ - where: { id: userId }, - withDeleted: options.withDeleted, - relations: { - metadata: true, - }, - }); + + return this.db + .selectFrom('users') + .select(columns) + .select(withMetadata) + .where('users.id', '=', userId) + .$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null)) + .executeTakeFirst() as Promise; } @GenerateSql() - async getAdmin(): Promise { - return this.userRepository.findOne({ where: { isAdmin: true } }); + getAdmin(): Promise { + return this.db + .selectFrom('users') + .select(columns) + .where('users.isAdmin', '=', true) + .where('users.deletedAt', 'is', null) + .executeTakeFirst() as Promise; } @GenerateSql() async hasAdmin(): Promise { - return this.userRepository.exists({ where: { isAdmin: true } }); + const admin = await this.db + .selectFrom('users') + .select('users.id') + .where('users.isAdmin', '=', true) + .where('users.deletedAt', 'is', null) + .executeTakeFirst(); + + return !!admin; } @GenerateSql({ params: [DummyValue.EMAIL] }) - async getByEmail(email: string, withPassword?: boolean): Promise { - const builder = this.userRepository.createQueryBuilder('user').where({ email }); - - if (withPassword) { - builder.addSelect('user.password'); - } - - return builder.getOne(); + getByEmail(email: string, withPassword?: boolean): Promise { + return this.db + .selectFrom('users') + .select(columns) + .$if(!!withPassword, (eb) => eb.select('password')) + .where('email', '=', email) + .where('users.deletedAt', 'is', null) + .executeTakeFirst() as Promise; } @GenerateSql({ params: [DummyValue.STRING] }) - async getByStorageLabel(storageLabel: string): Promise { - return this.userRepository.findOne({ where: { storageLabel } }); + getByStorageLabel(storageLabel: string): Promise { + return this.db + .selectFrom('users') + .select(columns) + .where('users.storageLabel', '=', storageLabel) + .where('users.deletedAt', 'is', null) + .executeTakeFirst() as Promise; } @GenerateSql({ params: [DummyValue.STRING] }) - async getByOAuthId(oauthId: string): Promise { - return this.userRepository.findOne({ where: { oauthId } }); - } - - async getDeletedUsers(): Promise { - return this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } }); - } - - async getList({ withDeleted }: UserListFilter = {}): Promise { - return this.userRepository.find({ - withDeleted, - order: { - createdAt: 'DESC', - }, - relations: { - metadata: true, - }, - }); - } - - create(user: Partial): Promise { - return this.save(user); - } - - // TODO change to (user: Partial) - update(id: string, user: Partial): Promise { - return this.save({ ...user, id }); + getByOAuthId(oauthId: string): Promise { + return this.db + .selectFrom('users') + .select(columns) + .where('users.oauthId', '=', oauthId) + .where('users.deletedAt', 'is', null) + .executeTakeFirst() as Promise; + } + + getDeletedUsers(): Promise { + return this.db + .selectFrom('users') + .select(columns) + .where('users.deletedAt', 'is not', null) + .execute() as unknown as Promise; + } + + getList({ withDeleted }: UserListFilter = {}): Promise { + return this.db + .selectFrom('users') + .select(columns) + .select(withMetadata) + .$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null)) + .orderBy('createdAt', 'desc') + .execute() as unknown as Promise; + } + + async create(dto: Insertable): Promise { + return this.db + .insertInto('users') + .values(dto) + .returning(columns) + .executeTakeFirst() as unknown as Promise; + } + + update(id: string, dto: Updateable): Promise { + return this.db + .updateTable('users') + .set(dto) + .where('users.id', '=', asUuid(id)) + .where('users.deletedAt', 'is', null) + .returning(columns) + .returning(withMetadata) + .executeTakeFirst() as unknown as Promise; } async upsertMetadata(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { - await this.metadataRepository.upsert({ userId: id, key, value }, { conflictPaths: { userId: true, key: true } }); + await this.db + .insertInto('user_metadata') + .values({ userId: id, key, value } as Upsert) + .onConflict((oc) => + oc.columns(['userId', 'key']).doUpdateSet({ + key, + value, + } as Upsert), + ) + .execute(); } async deleteMetadata(id: string, key: T) { - await this.metadataRepository.delete({ userId: id, key }); + await this.db.deleteFrom('user_metadata').where('userId', '=', id).where('key', '=', key).execute(); } - async delete(user: UserEntity, hard?: boolean): Promise { - return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user); + delete(user: UserEntity, hard?: boolean): Promise { + return hard + ? (this.db.deleteFrom('users').where('id', '=', user.id).execute() as unknown as Promise) + : (this.db + .updateTable('users') + .set({ deletedAt: new Date() }) + .where('id', '=', user.id) + .execute() as unknown as Promise); } @GenerateSql() async getUserStats(): Promise { - const stats = await this.userRepository - .createQueryBuilder('users') - .select('users.id', 'userId') - .addSelect('users.name', 'userName') - .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') - .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') - .addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage') - .addSelect( - `COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'IMAGE'), 0)`, - 'usagePhotos', - ) - .addSelect( - `COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'VIDEO'), 0)`, - 'usageVideos', - ) - .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes') - .leftJoin('users.assets', 'assets') - .leftJoin('assets.exifInfo', 'exif') + const stats = (await this.db + .selectFrom('users') + .leftJoin('assets', 'assets.ownerId', 'users.id') + .leftJoin('exif', 'exif.assetId', 'assets.id') + .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes']) + .select((eb) => [ + eb.fn + .countAll() + .filterWhere((eb) => eb.and([eb('assets.type', '=', 'IMAGE'), eb('assets.isVisible', '=', true)])) + .as('photos'), + eb.fn + .countAll() + .filterWhere((eb) => eb.and([eb('assets.type', '=', 'VIDEO'), eb('assets.isVisible', '=', true)])) + .as('videos'), + eb.fn + .coalesce(eb.fn.sum('exif.fileSizeInByte').filterWhere('assets.libraryId', 'is', null), eb.lit(0)) + .as('usage'), + eb.fn + .coalesce( + eb.fn + .sum('exif.fileSizeInByte') + .filterWhere((eb) => eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', 'IMAGE')])), + eb.lit(0), + ) + .as('usagePhotos'), + eb.fn + .coalesce( + eb.fn + .sum('exif.fileSizeInByte') + .filterWhere((eb) => eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', 'VIDEO')])), + eb.lit(0), + ) + .as('usageVideos'), + ]) + .where('assets.deletedAt', 'is', null) .groupBy('users.id') - .orderBy('users.createdAt', 'ASC') - .getRawMany(); + .orderBy('users.createdAt', 'asc') + .execute()) as UserStatsQueryResponse[]; for (const stat of stats) { stat.photos = Number(stat.photos); @@ -137,41 +222,31 @@ export class UserRepository implements IUserRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) async updateUsage(id: string, delta: number): Promise { - await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta); + await this.db + .updateTable('users') + .set({ quotaUsageInBytes: sql`"quotaUsageInBytes" + ${delta}`, updatedAt: new Date() }) + .where('id', '=', asUuid(id)) + .where('users.deletedAt', 'is', null) + .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async syncUsage(id?: string) { - // we can't use parameters with getQuery, hence the template string - const subQuery = this.assetRepository - .createQueryBuilder('assets') - .select('COALESCE(SUM(exif."fileSizeInByte"), 0)') - .leftJoin('assets.exifInfo', 'exif') - .where('assets.ownerId = users.id') - .andWhere(`assets.libraryId IS NULL`) - .withDeleted(); - - const query = this.userRepository - .createQueryBuilder('users') - .leftJoin('users.assets', 'assets') - .update() - .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }); - - if (id) { - query.where('users.id = :id', { id }); - } + const query = this.db + .updateTable('users') + .set({ + quotaUsageInBytes: (eb) => + eb + .selectFrom('assets') + .leftJoin('exif', 'exif.assetId', 'assets.id') + .select((eb) => eb.fn.coalesce(eb.fn.sum('exif.fileSizeInByte'), eb.lit(0)).as('usage')) + .where('assets.libraryId', 'is', null) + .where('assets.ownerId', '=', eb.ref('users.id')), + updatedAt: new Date(), + }) + .where('users.deletedAt', 'is', null) + .$if(id != undefined, (eb) => eb.where('users.id', '=', asUuid(id!))); await query.execute(); } - - private async save(user: Partial) { - const { id } = await this.userRepository.save(user); - return this.userRepository.findOneOrFail({ - where: { id }, - withDeleted: true, - relations: { - metadata: true, - }, - }); - } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 12c93ee1273ed..bb1aac8e6e48b 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -153,7 +153,7 @@ describe(AlbumService.name, () => { }); it('should require valid userIds', async () => { - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect( sut.create(authStub.admin, { albumName: 'Empty album', @@ -299,7 +299,7 @@ describe(AlbumService.name, () => { it('should throw an error if the userId does not exist', async () => { accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect( sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-3' }] }), ).rejects.toBeInstanceOf(BadRequestException); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 06035b03a2937..6494a735b191f 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -96,7 +96,7 @@ describe('AuthService', () => { }); it('should check the user exists', async () => { - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); expect(userMock.getByEmail).toHaveBeenCalledTimes(1); }); @@ -144,7 +144,7 @@ describe('AuthService', () => { const auth = { user: { email: 'test@imimch.com' } } as AuthDto; const dto = { password: 'old-password', newPassword: 'new-password' }; - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException); }); @@ -227,7 +227,7 @@ describe('AuthService', () => { }); it('should sign up the admin', async () => { - userMock.getAdmin.mockResolvedValue(null); + userMock.getAdmin.mockResolvedValue(void 0); userMock.create.mockResolvedValue({ ...dto, id: 'admin', @@ -309,7 +309,7 @@ describe('AuthService', () => { it('should not accept a key without a user', async () => { sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired); - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -473,7 +473,7 @@ describe('AuthService', () => { it('should not allow auto registering', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthEnabled); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf( BadRequestException, ); @@ -510,7 +510,7 @@ describe('AuthService', () => { it('should allow auto registering by default', async () => { systemMock.get.mockResolvedValue(systemConfigStub.enabled); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); sessionMock.create.mockResolvedValue(sessionStub.valid); @@ -525,7 +525,7 @@ describe('AuthService', () => { it('should throw an error if user should be auto registered but the email claim does not exist', async () => { systemMock.get.mockResolvedValue(systemConfigStub.enabled); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); sessionMock.create.mockResolvedValue(sessionStub.valid); @@ -559,7 +559,7 @@ describe('AuthService', () => { it('should use the default quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); @@ -572,7 +572,7 @@ describe('AuthService', () => { it('should ignore an invalid storage quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' }); @@ -586,7 +586,7 @@ describe('AuthService', () => { it('should ignore a negative quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 }); @@ -600,7 +600,7 @@ describe('AuthService', () => { it('should not set quota for 0 quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 }); @@ -620,7 +620,7 @@ describe('AuthService', () => { it('should use a valid storage quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); - userMock.getByEmail.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index d6154976f9ffe..29b73954650cf 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -65,7 +65,7 @@ export class AuthService extends BaseService { if (user) { const isAuthenticated = this.validatePassword(dto.password, user); if (!isAuthenticated) { - user = null; + user = undefined; } } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 3630d69c1804f..82852c27e2400 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -1,8 +1,10 @@ import { BadRequestException, Inject } from '@nestjs/common'; +import { Insertable } from 'kysely'; import sanitize from 'sanitize-filename'; import { SystemConfig } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; +import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface'; @@ -131,7 +133,7 @@ export class BaseService { return checkAccess(this.accessRepository, request); } - async createUser(dto: Partial & { email: string }): Promise { + async createUser(dto: Insertable & { email: string }): Promise { const user = await this.userRepository.getByEmail(dto.email); if (user) { throw new BadRequestException('User exists'); @@ -144,7 +146,7 @@ export class BaseService { } } - const payload: Partial = { ...dto }; + const payload: Insertable = { ...dto }; if (payload.password) { payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); } diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index ef520070eaeb5..149b030e502a6 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -25,7 +25,7 @@ describe(CliService.name, () => { describe('resetAdminPassword', () => { it('should only work when there is an admin account', async () => { - userMock.getAdmin.mockResolvedValue(null); + userMock.getAdmin.mockResolvedValue(void 0); const ask = vitest.fn().mockResolvedValue('new-password'); await expect(sut.resetAdminPassword(ask)).rejects.toThrowError('Admin account does not exist'); diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 18a79108c4468..87e004845d66e 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -48,4 +48,8 @@ export class CliService extends BaseService { config.oauth.enabled = true; await this.updateConfig(config); } + + cleanup() { + return this.databaseRepository.shutdown(); + } } diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 70999332dc26a..6d2bc31cb7022 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -19,13 +19,13 @@ describe(UserAdminService.name, () => { ({ sut, jobMock, userMock } = newTestService(UserAdminService)); userMock.get.mockImplementation((userId) => - Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), + Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? undefined), ); }); describe('create', () => { it('should not create a user if there is no local admin account', async () => { - userMock.getAdmin.mockResolvedValueOnce(null); + userMock.getAdmin.mockResolvedValueOnce(void 0); await expect( sut.create({ @@ -66,8 +66,8 @@ describe(UserAdminService.name, () => { email: 'immich@test.com', storageLabel: 'storage_label', }; - userMock.getByEmail.mockResolvedValue(null); - userMock.getByStorageLabel.mockResolvedValue(null); + userMock.getByEmail.mockResolvedValue(void 0); + userMock.getByStorageLabel.mockResolvedValue(void 0); userMock.update.mockResolvedValue(userStub.user1); await sut.update(authStub.user1, userStub.user1.id, update); @@ -108,7 +108,7 @@ describe(UserAdminService.name, () => { }); it('update user information should throw error if user not found', async () => { - userMock.get.mockResolvedValueOnce(null); + userMock.get.mockResolvedValueOnce(void 0); await expect( sut.update(authStub.admin, userStub.user1.id, { shouldChangePassword: true }), @@ -118,7 +118,7 @@ describe(UserAdminService.name, () => { describe('delete', () => { it('should throw error if user could not be found', async () => { - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException); expect(userMock.delete).not.toHaveBeenCalled(); @@ -166,7 +166,7 @@ describe(UserAdminService.name, () => { describe('restore', () => { it('should throw error if user could not be found', async () => { - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException); expect(userMock.update).not.toHaveBeenCalled(); }); diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 08b663046bc31..cb7c2f08ada37 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -33,7 +33,7 @@ describe(UserService.name, () => { ({ sut, albumMock, jobMock, storageMock, systemMock, userMock } = newTestService(UserService)); userMock.get.mockImplementation((userId) => - Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), + Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? undefined), ); }); @@ -81,7 +81,7 @@ describe(UserService.name, () => { }); it('should throw an error if a user is not found', async () => { - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(BadRequestException); expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false }); }); @@ -100,7 +100,7 @@ describe(UserService.name, () => { describe('createProfileImage', () => { it('should throw an error if the user does not exist', async () => { const file = { path: '/profile/path' } as Express.Multer.File; - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(BadRequestException); @@ -155,7 +155,7 @@ describe(UserService.name, () => { describe('getUserProfileImage', () => { it('should throw an error if the user does not exist', async () => { - userMock.get.mockResolvedValue(null); + userMock.get.mockResolvedValue(void 0); await expect(sut.getProfileImage(userStub.admin.id)).rejects.toBeInstanceOf(BadRequestException); diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index bfb931105a326..c135772518681 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -4,6 +4,7 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked => { return { init: vitest.fn(), + shutdown: vitest.fn(), reconnect: vitest.fn(), getExtensionVersion: vitest.fn(), getExtensionVersionRange: vitest.fn(), diff --git a/web/package-lock.json b/web/package-lock.json index b25947dd3dc2f..9450b76834318 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -80,7 +80,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", "typescript": "^5.3.3" } }, From 36eef9807b47b9ba64343cc825c4d77172e2c1b3 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 13 Jan 2025 20:38:11 -0500 Subject: [PATCH 026/184] fix: version history sql (#15321) --- server/src/queries/version.history.repository.sql | 8 ++++++++ server/src/repositories/version-history.repository.ts | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/queries/version.history.repository.sql b/server/src/queries/version.history.repository.sql index 2e898cac31f86..a9805e8c2530e 100644 --- a/server/src/queries/version.history.repository.sql +++ b/server/src/queries/version.history.repository.sql @@ -15,3 +15,11 @@ from "version_history" order by "createdAt" desc + +-- VersionHistoryRepository.create +insert into + "version_history" ("version") +values + ($1) +returning + * diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts index a5016873508cd..e6ec8edcf4406 100644 --- a/server/src/repositories/version-history.repository.ts +++ b/server/src/repositories/version-history.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; +import { Insertable, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; -import { DB } from 'src/db'; -import { DummyValue, GenerateSql } from 'src/decorators'; +import { DB, VersionHistory } from 'src/db'; +import { GenerateSql } from 'src/decorators'; import { VersionHistoryEntity } from 'src/entities/version-history.entity'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; @@ -20,8 +20,8 @@ export class VersionHistoryRepository implements IVersionHistoryRepository { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); } - @GenerateSql({ params: [DummyValue.STRING] }) - create(version: Omit): Promise { + @GenerateSql({ params: [{ version: 'v1.123.0' }] }) + create(version: Insertable): Promise { return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow(); } } From 79726acc7232e539f780f633b83f8c002a39b01d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jan 2025 19:45:52 -0600 Subject: [PATCH 027/184] refactor: migrate sessions repository to kysely (#15268) * wip: search * wip: getByToken * wip: getByToken * wip: getByUserId * wip: create/update/delete * remove unused code * clean up and pr feedback * fix: test * fix: e2e test * pr feedback --- e2e/src/api/specs/user.e2e-spec.ts | 4 + server/src/entities/session.entity.ts | 36 +++++ server/src/interfaces/session.interface.ts | 8 +- server/src/queries/session.repository.sql | 147 +++++++++++------- server/src/repositories/session.repository.ts | 78 ++++++---- server/src/services/auth.service.spec.ts | 4 +- server/src/services/auth.service.ts | 4 +- 7 files changed, 185 insertions(+), 96 deletions(-) diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 1964dc6793642..9cffa5d754d1f 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -129,6 +129,8 @@ describe('/users', () => { expect(body).toEqual({ ...before, updatedAt: expect.any(String), + profileChangedAt: expect.any(String), + createdAt: expect.any(String), name: 'Name', }); }); @@ -177,6 +179,8 @@ describe('/users', () => { ...before, email: 'non-admin@immich.cloud', updatedAt: expect.anything(), + createdAt: expect.anything(), + profileChangedAt: expect.anything(), }); }); }); diff --git a/server/src/entities/session.entity.ts b/server/src/entities/session.entity.ts index 1cc9ad98572ab..e21c6d52ba469 100644 --- a/server/src/entities/session.entity.ts +++ b/server/src/entities/session.entity.ts @@ -1,3 +1,5 @@ +import { ExpressionBuilder } from 'kysely'; +import { DB } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; @@ -27,3 +29,37 @@ export class SessionEntity { @Column({ default: '' }) deviceOS!: string; } + +const userColumns = [ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', +] as const; + +export const withUser = (eb: ExpressionBuilder) => { + return eb + .selectFrom('users') + .select(userColumns) + .select((eb) => + eb + .selectFrom('user_metadata') + .whereRef('users.id', '=', 'user_metadata.userId') + .select((eb) => eb.fn('array_agg', [eb.table('user_metadata')]).as('metadata')) + .as('metadata'), + ) + .whereRef('users.id', '=', 'sessions.userId') + .where('users.deletedAt', 'is', null) + .as('user'); +}; diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts index 33b48045a2376..8d695fbfc29c0 100644 --- a/server/src/interfaces/session.interface.ts +++ b/server/src/interfaces/session.interface.ts @@ -1,3 +1,5 @@ +import { Insertable, Updateable } from 'kysely'; +import { Sessions } from 'src/db'; import { SessionEntity } from 'src/entities/session.entity'; export const ISessionRepository = 'ISessionRepository'; @@ -7,9 +9,9 @@ export type SessionSearchOptions = { updatedBefore: Date }; export interface ISessionRepository { search(options: SessionSearchOptions): Promise; - create>(dto: T): Promise; - update>(dto: T): Promise; + create(dto: Insertable): Promise; + update(id: string, dto: Updateable): Promise; delete(id: string): Promise; - getByToken(token: string): Promise; + getByToken(token: string): Promise; getByUserId(userId: string): Promise; } diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 2f0613b4d0398..b928195e72009 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -1,64 +1,97 @@ -- NOTE: This file is auto generated by ./sql-generator -- SessionRepository.search -SELECT - "SessionEntity"."id" AS "SessionEntity_id", - "SessionEntity"."userId" AS "SessionEntity_userId", - "SessionEntity"."createdAt" AS "SessionEntity_createdAt", - "SessionEntity"."updatedAt" AS "SessionEntity_updatedAt", - "SessionEntity"."deviceType" AS "SessionEntity_deviceType", - "SessionEntity"."deviceOS" AS "SessionEntity_deviceOS" -FROM - "sessions" "SessionEntity" -WHERE - (("SessionEntity"."updatedAt" <= $1)) +select + * +from + "sessions" +where + "sessions"."updatedAt" <= $1 -- SessionRepository.getByToken -SELECT DISTINCT - "distinctAlias"."SessionEntity_id" AS "ids_SessionEntity_id" -FROM - ( - SELECT - "SessionEntity"."id" AS "SessionEntity_id", - "SessionEntity"."userId" AS "SessionEntity_userId", - "SessionEntity"."createdAt" AS "SessionEntity_createdAt", - "SessionEntity"."updatedAt" AS "SessionEntity_updatedAt", - "SessionEntity"."deviceType" AS "SessionEntity_deviceType", - "SessionEntity"."deviceOS" AS "SessionEntity_deviceOS", - "SessionEntity__SessionEntity_user"."id" AS "SessionEntity__SessionEntity_user_id", - "SessionEntity__SessionEntity_user"."name" AS "SessionEntity__SessionEntity_user_name", - "SessionEntity__SessionEntity_user"."isAdmin" AS "SessionEntity__SessionEntity_user_isAdmin", - "SessionEntity__SessionEntity_user"."email" AS "SessionEntity__SessionEntity_user_email", - "SessionEntity__SessionEntity_user"."storageLabel" AS "SessionEntity__SessionEntity_user_storageLabel", - "SessionEntity__SessionEntity_user"."oauthId" AS "SessionEntity__SessionEntity_user_oauthId", - "SessionEntity__SessionEntity_user"."profileImagePath" AS "SessionEntity__SessionEntity_user_profileImagePath", - "SessionEntity__SessionEntity_user"."shouldChangePassword" AS "SessionEntity__SessionEntity_user_shouldChangePassword", - "SessionEntity__SessionEntity_user"."createdAt" AS "SessionEntity__SessionEntity_user_createdAt", - "SessionEntity__SessionEntity_user"."deletedAt" AS "SessionEntity__SessionEntity_user_deletedAt", - "SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status", - "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", - "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", - "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes", - "SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt", - "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId", - "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key", - "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value" - FROM - "sessions" "SessionEntity" - LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId" - AND ( - "SessionEntity__SessionEntity_user"."deletedAt" IS NULL - ) - LEFT JOIN "user_metadata" "469e6aa7ff79eff78f8441f91ba15bb07d3634dd" ON "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" = "SessionEntity__SessionEntity_user"."id" - WHERE - (("SessionEntity"."token" = $1)) - ) "distinctAlias" -ORDER BY - "SessionEntity_id" ASC -LIMIT - 1 +select + "sessions".*, + to_json("user") as "user" +from + "sessions" + inner join lateral ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt", + ( + select + array_agg("user_metadata") as "metadata" + from + "user_metadata" + where + "users"."id" = "user_metadata"."userId" + ) as "metadata" + from + "users" + where + "users"."id" = "sessions"."userId" + and "users"."deletedAt" is null + ) as "user" on true +where + "sessions"."token" = $1 + +-- SessionRepository.getByUserId +select + "sessions".*, + to_json("user") as "user" +from + "sessions" + inner join lateral ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt", + ( + select + array_agg("user_metadata") as "metadata" + from + "user_metadata" + where + "users"."id" = "user_metadata"."userId" + ) as "metadata" + from + "users" + where + "users"."id" = "sessions"."userId" + and "users"."deletedAt" is null + ) as "user" on true +where + "sessions"."userId" = $1 +order by + "sessions"."updatedAt" desc, + "sessions"."createdAt" desc -- SessionRepository.delete -DELETE FROM "sessions" -WHERE - "id" = $1 +delete from "sessions" +where + "id" = $1::uuid diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 3a0af1ef69d0f..3e6c8977212a7 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,56 +1,70 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Sessions } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { SessionEntity } from 'src/entities/session.entity'; +import { SessionEntity, withUser } from 'src/entities/session.entity'; import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface'; -import { LessThanOrEqual, Repository } from 'typeorm'; +import { asUuid } from 'src/utils/database'; @Injectable() export class SessionRepository implements ISessionRepository { - constructor(@InjectRepository(SessionEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} - @GenerateSql({ params: [DummyValue.DATE] }) + @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) search(options: SessionSearchOptions): Promise { - return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } }); + return this.db + .selectFrom('sessions') + .selectAll() + .where('sessions.updatedAt', '<=', options.updatedBefore) + .execute() as Promise; } @GenerateSql({ params: [DummyValue.STRING] }) - getByToken(token: string): Promise { - return this.repository.findOne({ - where: { token }, - relations: { - user: { - metadata: true, - }, - }, - }); + getByToken(token: string): Promise { + return this.db + .selectFrom('sessions') + .innerJoinLateral(withUser, (join) => join.onTrue()) + .selectAll('sessions') + .select((eb) => eb.fn.toJson('user').as('user')) + .where('sessions.token', '=', token) + .executeTakeFirst() as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string): Promise { - return this.repository.find({ - where: { - userId, - }, - relations: { - user: true, - }, - order: { - updatedAt: 'desc', - createdAt: 'desc', - }, - }); + return this.db + .selectFrom('sessions') + .innerJoinLateral(withUser, (join) => join.onTrue()) + .selectAll('sessions') + .select((eb) => eb.fn.toJson('user').as('user')) + .where('sessions.userId', '=', userId) + .orderBy('sessions.updatedAt', 'desc') + .orderBy('sessions.createdAt', 'desc') + .execute() as unknown as Promise; } - create>(dto: T): Promise { - return this.repository.save(dto); + async create(dto: Insertable): Promise { + const { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } = await this.db + .insertInto('sessions') + .values(dto) + .returningAll() + .executeTakeFirstOrThrow(); + + return { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } as SessionEntity; } - update>(dto: T): Promise { - return this.repository.save(dto); + update(id: string, dto: Updateable): Promise { + return this.db + .updateTable('sessions') + .set(dto) + .where('sessions.id', '=', asUuid(id)) + .returningAll() + .executeTakeFirstOrThrow() as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); } } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 6494a735b191f..da25663f38750 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -354,7 +354,7 @@ describe('AuthService', () => { describe('validate - user token', () => { it('should throw if no token is found', async () => { - sessionMock.getByToken.mockResolvedValue(null); + sessionMock.getByToken.mockResolvedValue(void 0); await expect( sut.authenticate({ headers: { 'x-immich-user-token': 'auth_token' }, @@ -399,7 +399,7 @@ describe('AuthService', () => { metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), ).resolves.toBeDefined(); - expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); + expect(sessionMock.update.mock.calls[0][1]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 29b73954650cf..9999c16f64ba2 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -331,7 +331,7 @@ export class AuthService extends BaseService { const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); if (diff.hours > 1) { - await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); + await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() }); } return { user: session.user, session }; @@ -346,9 +346,9 @@ export class AuthService extends BaseService { await this.sessionRepository.create({ token, - user, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, + userId: user.id, }); return mapLoginResponse(user, key); From b74f013b5394cc4d0710dc285201dc2372222c42 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:57:19 -0500 Subject: [PATCH 028/184] fix(docs): database name for restore commands (#15276) * cleanup dbname * 2 * Update database-queries.md * Update backup-and-restore.md * Update backup-and-restore.md --- docs/docs/FAQ.mdx | 4 ++-- .../docs/administration/backup-and-restore.md | 24 +++++++++---------- docs/docs/guides/database-queries.md | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index f82bc74f5ffa9..71ddcf0d33f13 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -425,7 +425,7 @@ A result of `on` means that checksums are enabled. Check if checksums are enabled ```bash -docker exec -it immich_postgres psql --dbname=immich --username= --command="show data_checksums" +docker exec -it immich_postgres psql --dbname=postgres --username= --command="show data_checksums" data_checksums ---------------- on @@ -440,7 +440,7 @@ If checksums are enabled, you can check the status of the database with the foll Check for database corruption ```bash -docker exec -it immich_postgres psql --dbname=immich --username= --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL" +docker exec -it immich_postgres psql --dbname=postgres --username= --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL" datname | checksum_failures | checksum_last_failure -----------+-------------------+----------------------- postgres | 0 | diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 1b1775018efe3..cd58604e1f4d6 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -55,7 +55,7 @@ sleep 10 # Wait for Postgres server to start up # Check the database user if you deviated from the default gunzip < "/path/to/backup/dump.sql.gz" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ -| docker exec -i immich_postgres psql --username=postgres # Restore Backup +| docker exec -i immich_postgres psql --dbname=postgres --username= # Restore Backup docker compose up -d # Start remainder of Immich apps ``` @@ -70,18 +70,18 @@ docker compose up -d # Start remainder of Immich apps docker compose down -v # CAUTION! Deletes all Immich data to start from scratch ## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database # Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch -## You should mount the backup (as a volume, example: - 'C:\path\to\backup\dump.sql':/dump.sql) into the immich_postgres container using the docker-compose.yml -docker compose pull # Update to latest version of Immich (if desired) -docker compose create # Create Docker containers for Immich apps without running them -docker start immich_postgres # Start Postgres server -sleep 10 # Wait for Postgres server to start up -docker exec -it immich_postgres bash # Enter the Docker shell and run the following command -# Check the database user if you deviated from the default -cat "/dump.sql" \ +## You should mount the backup (as a volume, example: `- 'C:\path\to\backup\dump.sql:/dump.sql'`) into the immich_postgres container using the docker-compose.yml +docker compose pull # Update to latest version of Immich (if desired) +docker compose create # Create Docker containers for Immich apps without running them +docker start immich_postgres # Start Postgres server +sleep 10 # Wait for Postgres server to start up +docker exec -it immich_postgres bash # Enter the Docker shell and run the following command +# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip` +cat < "/dump.sql" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ -| psql --username=postgres # Restore Backup -exit # Exit the Docker shell -docker compose up -d # Start remainder of Immich apps +| psql --dbname=postgres --username= # Restore Backup +exit # Exit the Docker shell +docker compose up -d # Start remainder of Immich apps ``` diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 0e58d84f90c01..e71fa21c8b041 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -5,9 +5,9 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo ::: :::tip -Run `docker exec -it immich_postgres psql --dbname=immich --username=` to connect to the database via the container directly. +Run `docker exec -it immich_postgres psql --dbname= --username=` to connect to the database via the container directly. -(Replace `` with the value from your [`.env` file](/docs/install/environment-variables#database)). +(Replace `` and `` with the values from your [`.env` file](/docs/install/environment-variables#database)). ::: ## Assets From 28b08ed4178b4d7d7c6fbbe994b89ab2de7f4ae3 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:23:12 +0100 Subject: [PATCH 029/184] refactor: migrate audit repository to kysely (#15269) --- server/src/queries/audit.repository.sql | 21 +++++++++++ server/src/repositories/audit.repository.ts | 39 ++++++++++++--------- 2 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 server/src/queries/audit.repository.sql diff --git a/server/src/queries/audit.repository.sql b/server/src/queries/audit.repository.sql new file mode 100644 index 0000000000000..3c83d2d3e8916 --- /dev/null +++ b/server/src/queries/audit.repository.sql @@ -0,0 +1,21 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AuditRepository.getAfter +select distinct + on ("audit"."entityId", "audit"."entityType") "audit"."entityId" +from + "audit" +where + "audit"."createdAt" > $1 + and "audit"."action" = $2 + and "audit"."entityType" = $3 + and "audit"."ownerId" in ($4) +order by + "audit"."entityId" desc, + "audit"."entityType" desc, + "audit"."createdAt" desc + +-- AuditRepository.removeBefore +delete from "audit" +where + "createdAt" < $1 diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts index ac73c3a8b9d82..5731087aef554 100644 --- a/server/src/repositories/audit.repository.ts +++ b/server/src/repositories/audit.repository.ts @@ -1,31 +1,38 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AuditEntity } from 'src/entities/audit.entity'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { DatabaseAction, EntityType } from 'src/enum'; import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface'; -import { In, LessThan, MoreThan, Repository } from 'typeorm'; @Injectable() export class AuditRepository implements IAuditRepository { - constructor(@InjectRepository(AuditEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ + params: [ + DummyValue.DATE, + { action: DatabaseAction.CREATE, entityType: EntityType.ASSET, userIds: [DummyValue.UUID] }, + ], + }) async getAfter(since: Date, options: AuditSearch): Promise { - const records = await this.repository - .createQueryBuilder('audit') - .where({ - createdAt: MoreThan(since), - action: options.action, - entityType: options.entityType, - ownerId: In(options.userIds), - }) + const records = await this.db + .selectFrom('audit') + .where('audit.createdAt', '>', since) + .$if(!!options.action, (qb) => qb.where('audit.action', '=', options.action!)) + .$if(!!options.entityType, (qb) => qb.where('audit.entityType', '=', options.entityType!)) + .where('audit.ownerId', 'in', options.userIds) .distinctOn(['audit.entityId', 'audit.entityType']) - .orderBy('audit.entityId, audit.entityType, audit.createdAt', 'DESC') + .orderBy(['audit.entityId desc', 'audit.entityType desc', 'audit.createdAt desc']) .select('audit.entityId') - .getMany(); + .execute(); - return records.map((r) => r.entityId); + return records.map(({ entityId }) => entityId); } + @GenerateSql({ params: [DummyValue.DATE] }) async removeBefore(before: Date): Promise { - await this.repository.delete({ createdAt: LessThan(before) }); + await this.db.deleteFrom('audit').where('createdAt', '<', before).execute(); } } From dc53e2a9b94d197a4544ad8a8b5d66ef0fedb2bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 21:32:52 -0500 Subject: [PATCH 030/184] chore(deps): update docker.io/redis:6.2-alpine docker digest to 905c4ee (#15193) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4b8453ce58b0c..df3fc3ce56d4d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -48,7 +48,7 @@ services: redis: container_name: immich_redis - image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8 + image: docker.io/redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae healthcheck: test: redis-cli ping || exit 1 restart: always From 3b0622021996ba9c0001310136ef56354ca18986 Mon Sep 17 00:00:00 2001 From: Zer0x00 Date: Tue, 14 Jan 2025 03:42:32 +0100 Subject: [PATCH 031/184] feat: Upgrade devcontainer setup (#14419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Upgrade devcontainer * Style: Format devcontainer.json * Chore: Remove settings from devcontainer * chore: add shebang * chore: fix shellcheck --------- Co-authored-by: Bünyamin Olgun Co-authored-by: Jason Rasmussen --- .devcontainer/.gitignore | 2 ++ .devcontainer/Dockerfile | 14 ++++++++ .devcontainer/devcontainer.json | 42 ++++++++++++---------- .devcontainer/docker-compose.yml | 8 +++++ .devcontainer/scripts/initializeCommand.sh | 6 ++++ .devcontainer/scripts/onCreateCommand.sh | 25 +++++++++++++ 6 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 .devcontainer/.gitignore create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/scripts/initializeCommand.sh create mode 100644 .devcontainer/scripts/onCreateCommand.sh diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 0000000000000..6bf3b5d9e5c1d --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,2 @@ +.env +library \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9ae47b93759a4..a107c1ac3ada5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,2 +1,16 @@ ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:9791f4aa527774bc370c6bd2f6705ce5a686f1e6f204badd8dfaacce28c631ae FROM ${BASEIMAGE} + +# Flutter SDK +# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux +ENV FLUTTER_CHANNEL="stable" +ENV FLUTTER_VERSION="3.24.5" +ENV FLUTTER_HOME=/flutter +ENV PATH=${PATH}:${FLUTTER_HOME}/bin + +# Flutter SDK +RUN mkdir -p ${FLUTTER_HOME} \ + && curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \ + && tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \ + && rm flutter.tar.xz \ + && chown -R 1000:1000 ${FLUTTER_HOME} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b297f9a2d8cc1..2d567f033a81d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,26 @@ { - "name": "Immich devcontainers", - "build": { - "dockerfile": "Dockerfile", - "args": { - "BASEIMAGE": "mcr.microsoft.com/devcontainers/typescript-node:22" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "svelte.svelte-vscode" - ] - } - }, - "forwardPorts": [], - "postCreateCommand": "make install-all", - "remoteUser": "node" + "name": "Immich", + "service": "immich-devcontainer", + "dockerComposeFile": [ + "docker-compose.yml", + "../docker/docker-compose.dev.yml" + ], + "customizations": { + "vscode": { + "extensions": [ + "Dart-Code.dart-code", + "Dart-Code.flutter", + "dbaeumer.vscode-eslint", + "dcmdev.dcm-vscode-extension", + "esbenp.prettier-vscode", + "svelte.svelte-vscode" + ] + } + }, + "forwardPorts": [], + "initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh", + "onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh", + "overrideCommand": true, + "workspaceFolder": "/immich", + "remoteUser": "node" } - diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000000..25719641d2ddf --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,8 @@ +services: + immich-devcontainer: + build: + dockerfile: Dockerfile + extra_hosts: + - 'host.docker.internal:host-gateway' + volumes: + - ..:/immich:cached diff --git a/.devcontainer/scripts/initializeCommand.sh b/.devcontainer/scripts/initializeCommand.sh new file mode 100644 index 0000000000000..9d9d196696ab5 --- /dev/null +++ b/.devcontainer/scripts/initializeCommand.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# If .env file does not exist, create it by copying example.env from the docker folder +if [ ! -f ".devcontainer/.env" ]; then + cp docker/example.env .devcontainer/.env +fi diff --git a/.devcontainer/scripts/onCreateCommand.sh b/.devcontainer/scripts/onCreateCommand.sh new file mode 100644 index 0000000000000..2f898ec32e8b9 --- /dev/null +++ b/.devcontainer/scripts/onCreateCommand.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Enable multiarch for arm64 if necessary +if [ "$(dpkg --print-architecture)" = "arm64" ]; then + sudo dpkg --add-architecture amd64 && \ + sudo apt-get update && \ + sudo apt-get install -y --no-install-recommends \ + qemu-user-static \ + libc6:amd64 \ + libstdc++6:amd64 \ + libgcc1:amd64 +fi + +# Install DCM +wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg +sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list + +sudo apt-get update +sudo apt-get install dcm + +dart --disable-analytics + +# Install immich +cd /immich || exit +make install-all From e978b8c685009587d5e6fd6d0c75866823606aa6 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 14 Jan 2025 03:57:54 +0100 Subject: [PATCH 032/184] chore(web): update translations (#15145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ms/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Andreas Johansen Co-authored-by: AtmosphericIgnition Co-authored-by: Bezruchenko Simon Co-authored-by: Damian Krysta Co-authored-by: Denis Pacquier Co-authored-by: Dmitry Banny Co-authored-by: Dominik Mielcarek Co-authored-by: Erik Järlestrand Co-authored-by: Filip Hanes Co-authored-by: Gerardo Doro Co-authored-by: Hurricane-32 Co-authored-by: Indrek Haav Co-authored-by: Jendrik Köhler Co-authored-by: Jordi Masip Co-authored-by: Kenji Opdam Co-authored-by: Krisztián Co-authored-by: Leo Bottaro Co-authored-by: Linerly Co-authored-by: Michal Micech Co-authored-by: Miki Mrvos Co-authored-by: Milan Šalka Co-authored-by: Milo Germanus Co-authored-by: Máté Molnár Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Rui Co-authored-by: Santiago Co-authored-by: Shawn Co-authored-by: Vykintas Vyšniauskas Co-authored-by: Xo Co-authored-by: chamdim Co-authored-by: grgergo Co-authored-by: rezi nagro Co-authored-by: scudo Co-authored-by: stelle Co-authored-by: thehijacker Co-authored-by: waclaw66 Co-authored-by: Вячеслав Лукьяненко Co-authored-by: Пламен Марков Co-authored-by: தமிழ்நேரம் --- i18n/bg.json | 22 +- i18n/ca.json | 5 +- i18n/cs.json | 8 +- i18n/de.json | 10 +- i18n/el.json | 13 +- i18n/es.json | 4 + i18n/et.json | 9 + i18n/fr.json | 4 + i18n/he.json | 64 +++--- i18n/hu.json | 6 +- i18n/id.json | 4 + i18n/it.json | 17 +- i18n/lt.json | 179 +++++++++------ i18n/lv.json | 17 +- i18n/ms.json | 59 ++++- i18n/nb_NO.json | 259 ++++++++++++++++++++-- i18n/nl.json | 4 + i18n/pl.json | 16 +- i18n/pt.json | 6 +- i18n/pt_BR.json | 4 + i18n/ru.json | 4 + i18n/sk.json | 475 +++++++++++++++++++++++++++------------- i18n/sl.json | 4 + i18n/sr_Cyrl.json | 4 + i18n/sr_Latn.json | 4 + i18n/sv.json | 8 +- i18n/ta.json | 2 +- i18n/uk.json | 6 +- i18n/zh_SIMPLIFIED.json | 4 + 29 files changed, 916 insertions(+), 305 deletions(-) diff --git a/i18n/bg.json b/i18n/bg.json index 6d66f0901c132..47c1a825087cb 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -437,8 +437,8 @@ "birthdate_set_description": "Датата на раждане се използва за изчисляване на възрастта на този човек към момента на снимката.", "blurred_background": "Замъглен заден фон", "bugs_and_feature_requests": "Бъгове и заявки за функции", - "build": "Създаване", - "build_image": "Създаване на изображение", + "build": "Версия", + "build_image": "Docker версия", "bulk_delete_duplicates_confirmation": "Сигурни ли сте, че искате да изтриете масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще изтрие трайно всички други дубликати. Не можете да отмените това действие!", "bulk_keep_duplicates_confirmation": "Сигурни ли сте, че искате да запазите {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще потвърди всички групи дубликати, без да изтрива нищо.", "bulk_trash_duplicates_confirmation": "Сигурни ли сте, че искате да преместите в кошчето масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще премести в кошчето всички други дубликати.", @@ -523,6 +523,10 @@ "date_range": "Период от време", "day": "Ден", "deduplicate_all": "Дедупликиране на всички", + "deduplication_criteria_1": "Размер на снимката в байтове", + "deduplication_criteria_2": "Брой EXIF данни", + "deduplication_info": "Информация за дедупликацията", + "deduplication_info_description": "За автоматично предварително избиране на ресурси и премахване на дубликати на едро, разглеждаме:", "default_locale": "Локализация по подразбиране", "default_locale_description": "Форматиране на дати и числа в зависимост от местоположението на браузъра", "delete": "Изтрий", @@ -669,7 +673,7 @@ "unable_to_download_files": "Не могат да се изтеглят файловете", "unable_to_edit_exclusion_pattern": "Не може да се редактира шаблон за изключване", "unable_to_edit_import_path": "Пътят за импортиране не може да се редактира", - "unable_to_empty_trash": "Не може да изпразни кошчето", + "unable_to_empty_trash": "Неуспешно изпразване на кошчето", "unable_to_enter_fullscreen": "Не може да се отвори в цял екран", "unable_to_exit_fullscreen": "Не може да излезе от цял екран", "unable_to_get_comments_number": "Не може да получи брой коментари", @@ -765,7 +769,7 @@ "group_no": "Няма група", "group_owner": "Групиране по собственик", "group_year": "Групиране по година", - "has_quota": "Има лимит", + "has_quota": "Лимит", "hi_user": "Здравей, {name} {email}", "hide_all_people": "Скрий всички хора", "hide_gallery": "Скрий галерия", @@ -1009,7 +1013,7 @@ "purchase_button_select": "Избери", "purchase_failed_activation": "Неуспешна активация! Моля, проверете имейла си за правилния продуктов ключ!", "purchase_individual_description_1": "За индивидуален потребител", - "purchase_individual_description_2": "Поддръжнически статус", + "purchase_individual_description_2": "Статус на поддръжник", "purchase_individual_title": "Индивидуален", "purchase_input_suggestion": "Имате продуктов ключ? Въведете ключа по-долу", "purchase_license_subtitle": "Закупете Immich, за да подкрепите продължаващото развитие на услугата", @@ -1025,7 +1029,7 @@ "purchase_remove_server_product_key": "Премахни продуктовия ключ на сървъра", "purchase_remove_server_product_key_prompt": "Сигурни ли сте, че искате да премахнете продуктовия ключ на сървъра?", "purchase_server_description_1": "За целият сървър", - "purchase_server_description_2": "Статус на поддръжника", + "purchase_server_description_2": "Статус на поддръжник", "purchase_server_title": "Сървър", "purchase_settings_server_activated": "Продуктовият ключ на сървъра се управлява от администратора", "rating": "Оценка със звезди", @@ -1205,7 +1209,7 @@ "sort_people_by_similarity": "Сортиране на хора по прилика", "sort_recent": "Най-новата снимка", "sort_title": "Заглавие", - "source": "Източник", + "source": "Код", "stack": "Събери", "stack_duplicates": "Подреждане на дубликати", "stack_select_one_photo": "Избери една главна снимка за събраните снимки", @@ -1258,9 +1262,9 @@ "toggle_theme": "Превключване на тема", "total": "Общо", "total_usage": "Общо използвано", - "trash": "кошче", + "trash": "Кошче", "trash_all": "Изхвърли всички", - "trash_count": "Кошче {count, number}", + "trash_count": "В Кошчето {count, number}", "trash_delete_asset": "Вкарай в Кошчето/Изтрий елемент", "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.", "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# ден} other {# дни}}.", diff --git a/i18n/ca.json b/i18n/ca.json index 160a6cac8a748..7480f06664777 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -1,5 +1,5 @@ { - "about": "Sobre", + "about": "Quant a", "account": "Compte", "account_settings": "Configuració del compte", "acknowledge": "D'acord", @@ -523,6 +523,9 @@ "date_range": "Interval de dates", "day": "Dia", "deduplicate_all": "Desduplica-ho tot", + "deduplication_criteria_1": "Mida d'imatge en bytes", + "deduplication_criteria_2": "Quantitat de dades EXIF", + "deduplication_info_description": "Per preseleccionar recursos automàticament i eliminar els duplicats de manera massiva, ens fixem en:", "default_locale": "Localització predeterminada", "default_locale_description": "Format de dates i números segons la configuració del navegador", "delete": "Esborra", diff --git a/i18n/cs.json b/i18n/cs.json index edb9c3c105f2b..fbfbdf3bfc94c 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -523,6 +523,10 @@ "date_range": "Rozsah dat", "day": "Den", "deduplicate_all": "Odstranit všechny duplicity", + "deduplication_criteria_1": "Velikost obrázku v bajtech", + "deduplication_criteria_2": "Počet EXIF dat", + "deduplication_info": "Informace o deduplikaci", + "deduplication_info_description": "Pro automatický předvýběr položek a hromadné odstranění duplicit se zohledňuje:", "default_locale": "Výchozí jazyk", "default_locale_description": "Formátovat datumy a čísla podle místního prostředí prohlížeče", "delete": "Smazat", @@ -921,7 +925,7 @@ "oldest_first": "Nejstarší první", "onboarding": "Zahájení", "onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.", - "onboarding_theme_description": "Zvolte si barevné téma pro svou instanci. Můžete to později změnit v nastavení.", + "onboarding_theme_description": "Zvolte si barevný motiv pro svou instanci. Můžete to později změnit v nastavení.", "onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.", "onboarding_welcome_user": "Vítej, {user}", "online": "Online", @@ -1282,7 +1286,7 @@ "unselect_all": "Zrušit výběr všech", "unselect_all_duplicates": "Zrušit výběr všech duplicit", "unstack": "Zrušit seskupení", - "unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}", + "unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}", "untracked_files": "Nesledované soubory", "untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", "up_next": "To je prozatím vše", diff --git a/i18n/de.json b/i18n/de.json index 89eea9fd49cfb..45d5212ea0404 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -290,7 +290,7 @@ "transcoding_constant_rate_factor_description": "Video-Qualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.", "transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen", "transcoding_encoding_options": "Kodierungsoptionen", - "transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für Kodierte Videos", + "transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für kodierte Videos", "transcoding_hardware_acceleration": "Hardware-Beschleunigung", "transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität", "transcoding_hardware_decoding": "Hardware-Dekodierung", @@ -304,7 +304,7 @@ "transcoding_max_keyframe_interval_description": "Legt den maximalen Frame-Abstand zwischen Keyframes fest. Niedrigere Werte verschlechtern die Komprimierungseffizienz, verbessern aber die Suchzeiten und können die Qualität in Szenen mit schnellen Bewegungen verbessern. Bei 0 wird dieser Wert automatisch eingestellt.", "transcoding_optimal_description": "Videos mit einer höheren Auflösung als der Zielauflösung oder in einem nicht akzeptierten Format", "transcoding_policy": "Transkodierungsrichtlinie", - "transcoding_policy_description": "Bestimme, wann ein Video Transkodiert wird", + "transcoding_policy_description": "Bestimme, wann ein Video transkodiert wird", "transcoding_preferred_hardware_device": "Bevorzugtes Hardwaregerät", "transcoding_preferred_hardware_device_description": "Gilt nur für VAAPI und QSV. Legt den für die Hardware-Transkodierung verwendeten dri-Node fest.", "transcoding_preset_preset": "Voreinstellung (-preset)", @@ -313,7 +313,7 @@ "transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.", "transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format", "transcoding_settings": "Video-Transkodierungseinstellungen", - "transcoding_settings_description": "Auflösungs- und Kodierungsinformationen von Videodateien verwalten", + "transcoding_settings_description": "Verwalten welche Videos transkodiert werden und wie diese verarbeitet werden", "transcoding_target_resolution": "Ziel-Auflösung", "transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.", "transcoding_temporal_aq": "Temporäre AQ", @@ -523,10 +523,10 @@ "date_range": "Datumsbereich", "day": "Tag", "deduplicate_all": "Alle Duplikate entfernen", - "deduplication_info": "Deduplizierungsinformationen", - "deduplication_info_description": "Für die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berücksichtigen wir:", "deduplication_criteria_1": "Bildgröße in Bytes", "deduplication_criteria_2": "Anzahl der EXIF-Daten", + "deduplication_info": "Deduplizierungsinformationen", + "deduplication_info_description": "Für die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berücksichtigen wir:", "default_locale": "Standard-Sprache", "default_locale_description": "Datumsangaben und Zahlen basierend auf dem Gebietsschema des Browsers formatieren", "delete": "Löschen", diff --git a/i18n/el.json b/i18n/el.json index 6c0bcdf6b9c33..ed06812e77933 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -289,6 +289,8 @@ "transcoding_constant_rate_factor": "Σταθερός παράγοντας ρυθμού (-crf)", "transcoding_constant_rate_factor_description": "Επίπεδο ποιότητας βίντεο. Οι τυπικές τιμές είναι οι, 23 για το H.264, 28 για το HEVC, 31 για το VP9 και 35 για το AV1. Χαμηλότερες τιμές σημαίνουν καλύτερη ποιότητα, αλλά παράγουν μεγαλύτερα αρχεία.", "transcoding_disabled_description": "Να μην μετατραπεί κανένα βίντεο γιατί δύναται να προκαλέσει πρόβλημα αναπαραγωγής σε ορισμένες συσκευές/εφαρμογές", + "transcoding_encoding_options": "Επιλογές κωδικοποίησης", + "transcoding_encoding_options_description": "Ορίστε τους κωδικοποιητές, την ανάλυση, την ποιότητα και άλλες επιλογές για τα κωδικοποιημένα βίντεο", "transcoding_hardware_acceleration": "Επιτάχυνση υλικού", "transcoding_hardware_acceleration_description": "Πειραματικό· πολύ πιο γρήγορο, αλλά θα έχει χαμηλότερη ποιότητα με τον ίδιο ρυθμό μετάδοσης (bitrate)", "transcoding_hardware_decoding": "Αποκωδικοποίηση μέσω υλικού", @@ -301,6 +303,8 @@ "transcoding_max_keyframe_interval": "Μέγιστο χρονικό διάστημα μεταξύ των καρέ αναφοράς (keyframe)", "transcoding_max_keyframe_interval_description": "Ορίζει το μέγιστο διάστημα μεταξύ των καρέ αναφοράς. Χαμηλότερες τιμές μειώνουν την αποδοτικότητα συμπίεσης, αλλά βελτιώνουν τον χρόνο αναζήτησης και μπορεί να βελτιώσουν την ποιότητα σε σκηνές με γρήγορη κίνηση. Η τιμή 0 ρυθμίζει αυτό το διάστημα αυτόματα.", "transcoding_optimal_description": "Βίντεο με ανώτερη ανάλυση από την επιθυμητή ή σε μη αποδεκτή μορφή", + "transcoding_policy": "Πολιτική Μετακωδικοποίησης", + "transcoding_policy_description": "Ορίστε πότε θα γίνει η μετακωδικοποίηση ενός βίντεο", "transcoding_preferred_hardware_device": "Προτιμώμενη συσκευή", "transcoding_preferred_hardware_device_description": "Ισχύει μόνο για VAAPI και QSV. Ορίζει τον κόμβο DRI που χρησιμοποιείται για την επιτάχυνση υλικού κατά την κωδικοποίηση.", "transcoding_preset_preset": "Προκαθορισμένη ρύθμιση (-preset)", @@ -309,7 +313,7 @@ "transcoding_reference_frames_description": "Ο αριθμός των καρέ που χρησιμοποιούνται ως αναφορά κατά τη συμπίεση ενός δεδομένου καρέ. Υψηλότερες τιμές βελτιώνουν την αποδοτικότητα της συμπίεσης, αλλά επιβραδύνουν την κωδικοποίηση. Η τιμή 0 ρυθμίζει αυτό τον αριθμό, αυτόματα.", "transcoding_required_description": "Μόνο βίντεο που δεν είναι σε αποδεκτή μορφή", "transcoding_settings": "Ρυθμίσεις μετατροπής βίντεο", - "transcoding_settings_description": "Διαχείριση της ανάλυσης και των πληροφοριών κωδικοποίησης των αρχείων βίντεο", + "transcoding_settings_description": "Διαχείριση των βίντεο που θα μετακωδικοποιηθούν και του τρόπου επεξεργασίας τους", "transcoding_target_resolution": "Επιθυμητή ανάλυση", "transcoding_target_resolution_description": "Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά απαιτούν περισσότερο χρόνο για κωδικοποίηση, παράγουν μεγαλύτερα αρχεία και μπορεί να μειώσουν την απόκριση της εφαρμογής.", "transcoding_temporal_aq": "Χρονική Προσαρμοστική Ποιότητα AQ(Adaptive Quantization)", @@ -322,7 +326,7 @@ "transcoding_transcode_policy_description": "Πολιτική για το πότε πρέπει να μετατραπεί ένα βίντεο. Τα βίντεο HDR θα μετατρέπονται πάντα (εκτός αν η μετατροπή είναι απενεργοποιημένη).", "transcoding_two_pass_encoding": "Κωδικοποίηση δύο περασμάτων", "transcoding_two_pass_encoding_setting_description": "Μετατροπή σε δύο περάσματα για την παραγωγή βίντεο με καλύτερη κωδικοποίηση. Όταν είναι ενεργοποιημένος ο μέγιστος ρυθμός μετάδοσης (απαραίτητος για λειτουργία με H.264 και HEVC), αυτή η λειτουργία χρησιμοποιεί ένα εύρος ρυθμού μετάδοσης βάσει του μέγιστου ρυθμού μετάδοσης και αγνοεί το CRF. Στον κωδικοποιητή VP9, το CRF μπορεί να χρησιμοποιηθεί εάν ο μέγιστος ρυθμός μετάδοσης είναι απενεργοποιημένος.", - "transcoding_video_codec": "Κωδικοποιητής Βίντεο", + "transcoding_video_codec": "Κωδικοποιητής βίντεο", "transcoding_video_codec_description": "Ο VP9 έχει υψηλή απόδοση και συμβατότητα με τον ιστότοπο, αλλά απαιτεί περισσότερο χρόνο για μετατροπή. Ο HEVC έχει παρόμοια απόδοση, αλλά χαμηλότερη συμβατότητα με τον ιστότοπο. Ο H.264 είναι ευρέως συμβατός και γρήγορος στη μετατροπή, αλλά παράγει πολύ μεγαλύτερα αρχεία. Ο AV1 είναι ο πιο αποδοτικός κωδικοποιητής, αλλά δεν υποστηρίζεται σε παλαιότερες συσκευές.", "trash_enabled_description": "Ενεργοποίηση λειτουργιών Κάδου Απορριμμάτων", "trash_number_of_days": "Αριθμός ημερών", @@ -519,6 +523,10 @@ "date_range": "Εύρος ημερομηνιών", "day": "Ημέρα", "deduplicate_all": "Αφαίρεση όλων των διπλότυπων", + "deduplication_criteria_1": "Μέγεθος εικόνας σε byte", + "deduplication_criteria_2": "Αριθμός δεδομένων EXIF", + "deduplication_info": "Πληροφορίες Αφαίρεσης Διπλοτύπων", + "deduplication_info_description": "Για να προεπιλέξουμε αυτόματα τα αρχεία και να αφαιρέσουμε τα διπλότυπα σε μαζική επεξεργασία, εξετάζουμε σε:", "default_locale": "Προεπιλεγμένη Τοπική Ρύθμιση", "default_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς με βάση την τοπική ρύθμιση του προγράμματος περιήγησής σας", "delete": "Διαγραφή", @@ -755,6 +763,7 @@ "get_help": "Ζητήστε βοήθεια", "getting_started": "Ξεκινώντας", "go_back": "Πηγαίνετε πίσω", + "go_to_folder": "Μετάβαση στο φάκελο", "go_to_search": "Πηγαίνετε στην αναζήτηση", "group_albums_by": "Ομαδοποίηση άλμπουμ κατά...", "group_no": "Καμία ομοδοποίηση", diff --git a/i18n/es.json b/i18n/es.json index 02b9b2fad7a82..c619fbfeb8992 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -523,6 +523,10 @@ "date_range": "Rango de fechas", "day": "Día", "deduplicate_all": "Deduplicar todo", + "deduplication_criteria_1": "Tamaño de imagen en bytes", + "deduplication_criteria_2": "Conteo de datos EXIF", + "deduplication_info": "Información de Deduplicación", + "deduplication_info_description": "Para automáticamente preseleccionar recursos y eliminar duplicados en conjunto, nosotros consideramos lo siguiente:", "default_locale": "Configuración regional predeterminada", "default_locale_description": "Formatee fechas y números según la configuración regional de su navegador", "delete": "Eliminar", diff --git a/i18n/et.json b/i18n/et.json index 4d64da159ce14..ab17fad19fe4c 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -523,6 +523,10 @@ "date_range": "Kuupäevavahemik", "day": "Päev", "deduplicate_all": "Dedubleeri kõik", + "deduplication_criteria_1": "Pildi suurus baitides", + "deduplication_criteria_2": "EXIF andmete hulk", + "deduplication_info": "Dedubleerimise info", + "deduplication_info_description": "Üksuste automaatsel eelvalimisel ja duplikaatide eemaldamisel võetakse arvesse:", "default_locale": "Vaikimisi lokaat", "default_locale_description": "Vorminda kuupäevad ja numbrid vastavalt brauseri lokaadile", "delete": "Kustuta", @@ -748,6 +752,7 @@ "filetype": "Failitüüp", "filter_people": "Filtreeri isikuid", "find_them_fast": "Leia teda kiiresti nime järgi otsides", + "fix_incorrect_match": "Paranda ebaõige vaste", "folders": "Kaustad", "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", "forward": "Edasi", @@ -755,6 +760,7 @@ "get_help": "Küsi abi", "getting_started": "Alustamine", "go_back": "Tagasi", + "go_to_folder": "Mine kausta", "go_to_search": "Otsingusse", "group_albums_by": "Grupeeri albumid...", "group_no": "Ära grupeeri", @@ -1029,6 +1035,7 @@ "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", "reassing_hint": "Seosta valitud üksused olemasoleva isikuga", + "recent": "Hiljutine", "recent-albums": "Hiljutised albumid", "recent_searches": "Hiljutised otsingud", "refresh": "Värskenda", @@ -1189,6 +1196,7 @@ "sort_items": "Üksuste arv", "sort_modified": "Muutmise aeg", "sort_oldest": "Vanim foto", + "sort_people_by_similarity": "Sorteeri isikud sarnasuse järgi", "sort_recent": "Uusim foto", "sort_title": "Pealkiri", "source": "Lähtekood", @@ -1309,6 +1317,7 @@ "view_all_users": "Vaata kõiki kasutajaid", "view_in_timeline": "Vaata ajajoonel", "view_links": "Vaata linke", + "view_name": "Vaade", "view_next_asset": "Vaata järgmist üksust", "view_previous_asset": "Vaata eelmist üksust", "view_stack": "Vaata virna", diff --git a/i18n/fr.json b/i18n/fr.json index c4bab44ee3770..2b5635fd9155a 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -523,6 +523,10 @@ "date_range": "Plage de dates", "day": "Jour", "deduplicate_all": "Dédupliquer tout", + "deduplication_criteria_1": "Taille de l'image en octets", + "deduplication_criteria_2": "Nombre de données EXIF", + "deduplication_info": "Info de déduplication", + "deduplication_info_description": "Pour présélectionner automatiquement les médias et supprimer les doublons en masse, nous examinons :", "default_locale": "Région par défaut", "default_locale_description": "Afficher les dates et nombres en fonction des paramètres de votre navigateur", "delete": "Supprimer", diff --git a/i18n/he.json b/i18n/he.json index 6026a0b4b33f0..9993d6cf48d0b 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -131,7 +131,7 @@ "machine_learning_smart_search_description": "חפש תמונות באופן סמנטי באמצעות הטמעות של CLIP", "machine_learning_smart_search_enabled": "אפשר חיפוש חכם", "machine_learning_smart_search_enabled_description": "אם מושבת, תמונות לא יקודדו לחיפוש חכם.", - "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתן יותר מכתובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.", + "machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתנת יותר מכתובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.", "manage_concurrency": "נהל בו-זמניות", "manage_log_settings": "נהל הגדרות רישום ביומן", "map_dark_style": "עיצוב כהה", @@ -289,8 +289,8 @@ "transcoding_constant_rate_factor": "גורם קצב קבוע (-crf)", "transcoding_constant_rate_factor_description": "רמת איכות וידאו. ערכים אופייניים הם הערך 23 עבור H.264, הערך 28 עבור HEVC, הערך 31 עבור VP9, והערך 35 עבור AV1. נמוך יותר הוא טוב יותר, אבל מייצר קבצים גדולים יותר.", "transcoding_disabled_description": "אין להמיר את הקידוד של שום סרטון, עלול לגרום לכך שהניגון לא יפעל במכשירים מסוימים", - "transcoding_encoding_options": "אפשרויות הקידוד", - "transcoding_encoding_options_description": "הגדר מקודדים, רזולוציה, איכות ואפשרויות נוספות עבור הסרטונים המקודדים", + "transcoding_encoding_options": "אפשרויות קידוד", + "transcoding_encoding_options_description": "הגדר מקודדים, רזולוציה, איכות ואפשרויות אחרות עבור הסרטונים המקודדים", "transcoding_hardware_acceleration": "האצת חומרה", "transcoding_hardware_acceleration_description": "ניסיוני; המרה הרבה יותר מהירה, אבל תהיה באיכות נמוכה יותר באותו קצב סיביות", "transcoding_hardware_decoding": "פענוח חומרה", @@ -304,7 +304,7 @@ "transcoding_max_keyframe_interval_description": "מגדיר את מרחק הפריימים המרבי בין תמונות מפתח. ערכים נמוכים גורעים את יעילות הדחיסה, אך משפרים את זמני החיפוש ועשויים לשפר את האיכות בסצנות עם תנועה מהירה. 0 מגדיר ערך זה באופן אוטומטי.", "transcoding_optimal_description": "סרטונים גבוהים מרזולוציית היעד או לא בפורמט מקובל", "transcoding_policy": "מדיניות המרה", - "transcoding_policy_description": "הגדר מתי וידאו יעבור המרה", + "transcoding_policy_description": "הגדר מתי סרטון יעבור המרת קידוד", "transcoding_preferred_hardware_device": "מכשיר חומרה מועדף", "transcoding_preferred_hardware_device_description": "חל רק על VAAPI ו-QSV. מגדיר את צומת ה-dri המשמש להמרת קידוד של חומרה.", "transcoding_preset_preset": "הגדרות קבועות מראש (-preset)", @@ -313,7 +313,7 @@ "transcoding_reference_frames_description": "מספר הפריימים לייחוס בעת דחיסה של פריים נתון. ערכים גבוהים יותר משפרים את יעילות הדחיסה, אך מאטים את הקידוד. 0 מגדיר את הערך זה באופן אוטומטי.", "transcoding_required_description": "רק סרטונים שאינם בפורמט מקובל", "transcoding_settings": "הגדרות המרת קידוד סרטונים", - "transcoding_settings_description": "נהל אילו סרטונים לעבד וכיצד לעבד אותם", + "transcoding_settings_description": "נהל אילו סרטונים להמיר וכיצד לעבד אותם", "transcoding_target_resolution": "רזולוציה יעד", "transcoding_target_resolution_description": "רזולוציות גבוהות יותר יכולות לשמר פרטים רבים יותר אך לוקחות זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.", "transcoding_temporal_aq": "Temporal AQ", @@ -326,7 +326,7 @@ "transcoding_transcode_policy_description": "מדיניות לגבי מתי יש להמיר קידוד של סרטון. תמיד יומר הקידוד של סרטוני HDR (למעט אם המרת קידוד מושבתת).", "transcoding_two_pass_encoding": "קידוד בשני מעברים", "transcoding_two_pass_encoding_setting_description": "המר קידוד בשני מעברים כדי לייצר סרטונים מקודדים טוב יותר. כאשר קצב סיביות מרבי מופעל (נדרש כדי שזה יעבוד עם H.264 ו-HEVC), מצב זה משתמש בטווח קצב סיביות המבוסס על קצב הסיביות המרבי ומתעלם מ-CRF. עבור VP9, ניתן להשתמש ב-CRF אם קצב סיביות מרבי מושבת.", - "transcoding_video_codec": "מקודדי וידאו", + "transcoding_video_codec": "מקודד סרטון", "transcoding_video_codec_description": "ל-VP9 יש יעילות גבוהה ותאימות רשת, אבל לוקח יותר זמן להמיר את הקידוד עבורו. HEVC מתפקד באופן דומה, אך בעל תאימות רשת נמוכה יותר. H.264 תואם באופן נרחב ומהיר להמיר את קידודו, אבל הוא מייצר קבצים גדולים בהרבה. AV1 הוא הקידוד היעיל ביותר אך לוקה בתמיכה במכשירים ישנים יותר.", "trash_enabled_description": "הפעל את תכונות האשפה", "trash_number_of_days": "מספר הימים", @@ -522,35 +522,39 @@ "date_of_birth_saved": "תאריך לידה נשמר בהצלחה", "date_range": "טווח תאריכים", "day": "יום", - "deduplicate_all": "בטל כפילויות של הכל", - "default_locale": "אזור שפה ברירת מחדל", - "default_locale_description": "עצב תאריכים ומספרים על סמך אזור השפה של הדפדפן שלך", - "delete": "מחק", - "delete_album": "מחק אלבום", - "delete_api_key_prompt": "האם את/ה בטוח/ה שברצונך למחוק מפתח API זה?", - "delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק לצמיתות את הכפילויות האלה?", - "delete_key": "מחק מפתח", - "delete_library": "מחק ספרייה", - "delete_link": "מחק קישור", - "delete_others": "מחק אחרים", - "delete_shared_link": "מחק קישור משותף", - "delete_tag": "מחק תג", - "delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?", - "delete_user": "מחק משתמש", - "deleted_shared_link": "קישור משותף נמחק", - "deletes_missing_assets": "מוחק נכסים שחסרים בדיסק", + "deduplicate_all": "ביטול כל הכפילויות", + "deduplication_criteria_1": "גודל תמונה בבתים", + "deduplication_criteria_2": "ספירת נתוני EXIF", + "deduplication_info": "מידע על ביטול כפילויות", + "deduplication_info_description": "כדי לבחור מראש נכסים באופן אוטומטי ולהסיר כפילויות בכמות גדולה, אנו מסתכלים על:", + "default_locale": "שפת ברירת מחדל", + "default_locale_description": "פורמט תאריכים ומספרים מבוסס שפת הדפדפן שלך", + "delete": "הסרה", + "delete_album": "הסרת אלבום", + "delete_api_key_prompt": "האם ברצונך למחוק מפתח ה-API הזה?", + "delete_duplicates_confirmation": "האם ברצונך להסיר לצמיתות את הכפילויות האלה?", + "delete_key": "הסרת מפתח", + "delete_library": "הסרת ספרייה", + "delete_link": "הסרת קישור", + "delete_others": "הסרת אחרים", + "delete_shared_link": "הסרת קישור משותף", + "delete_tag": "הסרת תג", + "delete_tag_confirmation_prompt": "האם ברצונך להסיר תג {tagName}?", + "delete_user": "הסרת משתמש", + "deleted_shared_link": "קישור משותף הוסר", + "deletes_missing_assets": "מסיר נכסים שחסרים בדיסק", "description": "תיאור", "details": "פרטים", "direction": "כיוון", "disabled": "מושבת", "disallow_edits": "אל תאפשר עריכות", - "discord": "דיסקורד", - "discover": "גלה", - "dismiss_all_errors": "התעלם מכל השגיאות", - "dismiss_error": "התעלם מהשגיאה", - "display_options": "הצג אפשרויות", - "display_order": "סדר תצוגה", - "display_original_photos": "הצג תמונות מקוריות", + "discord": "Discord", + "discover": "גילוי", + "dismiss_all_errors": "התעלמות מכל השגיאות", + "dismiss_error": "התעלמות מהשגיאה", + "display_options": "הצגת אפשרויות", + "display_order": "סידור תצוגה", + "display_original_photos": "הצגת תמונות מקוריות", "display_original_photos_setting_description": "העדף להציג את התמונה המקורית בעת צפיית נכס במקום תמונות ממוזערות כאשר הנכס המקורי תומך בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.", "do_not_show_again": "אל תציג את ההודעה הזאת שוב", "documentation": "תיעוד", diff --git a/i18n/hu.json b/i18n/hu.json index f8de5bac258bc..7c1616c9a0a7e 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -29,7 +29,7 @@ "added_to_favorites_count": "{count, number} hozzáadva a kedvencekhez", "admin": { "add_exclusion_pattern_description": "Kihagyási minták (pattern) megadása. A *, ** és ? helyettesítő karakterek engedélyezettek. Pl. a \"Raw\" könyvtárban tárolt összes fájl kihagyásához használható a \"**/Raw/**\". Minden \".tif\" fájl kihagyása az összes mappában: \"**/*.tif\". Abszolút elérési útvonal kihagyása: \"/kihagyni/kivant/mappa/**\".", - "asset_offline_description": "Ez a külső képtárban lévő elem már nem található, ezért a lomtárba került. Ha a fájl a képtáron belül lett áthelyezve, akkor ellenőrizd, hogy továbbra is látható az idővonaladon. Az elem visszaállításához győződj meg róla, hogy az alábbi mappa az Immich számára elérhető, majd újra átfésültesd át a képtárat.", + "asset_offline_description": "Ez a külső képtárban lévő elem már nem található, ezért a lomtárba került. Ha a fájl a képtáron belül lett áthelyezve, akkor ellenőrizd, hogy továbbra is látható az idővonaladon. Az elem visszaállításához győződj meg róla, hogy az alábbi mappa az Immich számára elérhető, majd újra fésüld át a képtárat.", "authentication_settings": "Hitelesítési beállítások", "authentication_settings_description": "Jelszó, OAuth és egyéb hitelesítési beállítások kezelése", "authentication_settings_disable_all": "Biztosan letiltod az összes bejelentkezési módot? A bejelentkezés teljesen le lesz tiltva.", @@ -523,6 +523,10 @@ "date_range": "Dátum intervallum", "day": "Nap", "deduplicate_all": "Az Összes Deduplikálása", + "deduplication_criteria_1": "Kép mérete bájtokban", + "deduplication_criteria_2": "EXIF adatok mennyisége", + "deduplication_info": "Deduplikációs Infó", + "deduplication_info_description": "Az automatikus előválogatáshoz és a duplikátumok tömeges eltávolításához a következőket vizsgáljuk:", "default_locale": "Alapértelmezett Területi Beállítás", "default_locale_description": "Dátumok és számok formázása a böngésződ területi beállítása alapján", "delete": "Törlés", diff --git a/i18n/id.json b/i18n/id.json index b76dc95e24dda..41ef0b008cae0 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -523,6 +523,10 @@ "date_range": "Jangka tanggal", "day": "Hari", "deduplicate_all": "Deduplikat Semua", + "deduplication_criteria_1": "Ukuran gambar dalam bita", + "deduplication_criteria_2": "Hitungan data EXIF", + "deduplication_info": "Info deduplikasi", + "deduplication_info_description": "Untuk memilih aset secara otomatis dan menghapus duplikat secara massal, kami melihat:", "default_locale": "Lokal Bawaan", "default_locale_description": "Format tanggal dan angka berdasarkan lokal peramban Anda", "delete": "Hapus", diff --git a/i18n/it.json b/i18n/it.json index 11e86c4a852f8..bd05f8e55591e 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -289,6 +289,8 @@ "transcoding_constant_rate_factor": "Fattore di rateo costante (-crf)", "transcoding_constant_rate_factor_description": "Livello di qualità video. I valori tipici sono 23 per H.264, 28 per HEVC, 31 per VP9 e 35 per AV1. Un valore inferiore indica una qualità migliore, ma produce file di dimensioni maggiori.", "transcoding_disabled_description": "Non transcodificare alcun video, potrebbe rompere la riproduzione su alcuni client", + "transcoding_encoding_options": "Opzioni di codifica", + "transcoding_encoding_options_description": "Imposta codecs, risoluzione, qualità ed altre opzioni per i video codificati", "transcoding_hardware_acceleration": "Accelerazione Hardware", "transcoding_hardware_acceleration_description": "Sperimentale; molto più veloce, ma avrà una qualità inferiore allo stesso bitrate", "transcoding_hardware_decoding": "Decodifica hardware", @@ -301,6 +303,8 @@ "transcoding_max_keyframe_interval": "Intervallo massimo dei keyframe", "transcoding_max_keyframe_interval_description": "Imposta la distanza massima tra i keyframe. Valori più bassi peggiorano l'efficienza di compressione, però migliorano i tempi di ricerca e possono migliorare la qualità nelle scene con movimenti rapidi. 0 imposta questo valore automaticamente.", "transcoding_optimal_description": "Video con risoluzione più alta rispetto alla risoluzione desiderata o in formato non accettato", + "transcoding_policy": "Politiche di transcodifica", + "transcoding_policy_description": "Imposta quando un video sarà transcodificato", "transcoding_preferred_hardware_device": "Dispositivo hardware preferito", "transcoding_preferred_hardware_device_description": "Si applica solo a VAAPI e QSV. Imposta il nodo DRI utilizzato per la transcodifica hardware.", "transcoding_preset_preset": "Preset (-preset)", @@ -309,7 +313,7 @@ "transcoding_reference_frames_description": "Il numero di frame da prendere in considerazione nel comprimere un determinato frame. Valori più alti migliorano l'efficienza di compressione, ma rallentano la codifica. 0 imposta questo valore automaticamente.", "transcoding_required_description": "Solo video che non sono in un formato accettato", "transcoding_settings": "Impostazioni Trascodifica Video", - "transcoding_settings_description": "Gestisci le impostazioni di risoluzione e codifica dei file video", + "transcoding_settings_description": "Gestisci quali video transcodificare e come processarli", "transcoding_target_resolution": "Risoluzione desiderata", "transcoding_target_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedono più tempo per la codifica, producono file di dimensioni maggiori e possono ridurre la reattività dell'applicazione.", "transcoding_temporal_aq": "AQ temporale", @@ -322,7 +326,7 @@ "transcoding_transcode_policy_description": "Politica che determina quando un video deve essere trascodificato. I video HDR verranno sempre trascodificati (eccetto quando la trascodifica è disabilitata).", "transcoding_two_pass_encoding": "Codifica a due passaggi", "transcoding_two_pass_encoding_setting_description": "Trascodifica in due passaggi per produrre video codificati migliori. Quando il bitrate massimo è abilitato (necessario affinché funzioni con H.264 e HEVC), questa modalità utilizza un intervallo di bitrate basato sul bitrate massimo e ignora CRF. Per VP9, CRF può essere utilizzato se il bitrate massimo è disabilitato.", - "transcoding_video_codec": "Codifica Video", + "transcoding_video_codec": "Codec video", "transcoding_video_codec_description": "VP9 ha alta efficienza e compatibilità web, ma richiede più tempo per la trascodifica. HEVC ha prestazioni simili, ma una minore compatibilità web. H.264 è ampiamente compatibile e veloce da transcodificare, ma produce file molto più grandi. AV1 è il codec più efficiente, ma non è supportato sui dispositivi più vecchi.", "trash_enabled_description": "Abilita Funzionalità Cestino", "trash_number_of_days": "Numero di giorni", @@ -519,14 +523,18 @@ "date_range": "Intervallo di date", "day": "Giorno", "deduplicate_all": "De-duplica Tutti", + "deduplication_criteria_1": "Dimensione immagine in byte", + "deduplication_criteria_2": "Numero di dati EXIF", + "deduplication_info": "Informazioni di deduplicazione", + "deduplication_info_description": "Per preselezionare automaticamente gli asset e rimuovere i duplicati in massa, verifichiamo:", "default_locale": "Localizzazione preimpostata", - "default_locale_description": "Formatta la data e i numeri in base al locale del tuo browser", + "default_locale_description": "Formatta la data e i numeri in base alle impostazioni del tuo browser", "delete": "Elimina", "delete_album": "Elimina album", "delete_api_key_prompt": "Sei sicuro di voler eliminare questa chiave API?", "delete_duplicates_confirmation": "Sei sicuro di voler eliminare questi duplicati per sempre?", "delete_key": "Elimina chiave", - "delete_library": "Elimina Libreria", + "delete_library": "Elimina libreria", "delete_link": "Elimina link", "delete_others": "Elimina gli altri", "delete_shared_link": "Elimina link condiviso", @@ -755,6 +763,7 @@ "get_help": "Chiedi Aiuto", "getting_started": "Iniziamo", "go_back": "Torna indietro", + "go_to_folder": "Vai alla cartella", "go_to_search": "Vai alla ricerca", "group_albums_by": "Raggruppa album in base a...", "group_no": "Nessun raggruppamento", diff --git a/i18n/lt.json b/i18n/lt.json index cfb9701c1612c..7e24eedc743fd 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -1,5 +1,5 @@ { - "about": "Atnaujinti", + "about": "Apie", "account": "Paskyra", "account_settings": "Paskyros nustatymai", "acknowledge": "Patvirtinti", @@ -28,6 +28,7 @@ "added_to_favorites": "Pridėta prie mėgstamiausių", "added_to_favorites_count": "{count, plural, one {# pridėtas} few {# pridėti} other {# pridėta}} prie mėgstamiausių", "admin": { + "asset_offline_description": "Šis išorinės bibliotekos elementas nebepasiekiamas diske ir buvo perkeltas į šiukšliadėžę. Jei failas buvo perkeltas toje pačioje bibliotekoje, laiko skalėje rasite naują atitinkamą elementą. Jei norite šį elementą atkurti, įsitikinkite, kad Immich gali pasiekti failą žemiau nurodytu adresu, ir suvykdykite bibliotekos skanavimą.", "authentication_settings": "Autentifikavimo nustatymai", "authentication_settings_description": "Tvarkyti slaptažodžių, OAuth ir kitus autentifikavimo parametrus", "authentication_settings_disable_all": "Ar tikrai norite išjungti visus prisijungimo būdus? Prisijungimas bus visiškai išjungtas.", @@ -44,7 +45,7 @@ "confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.", "confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?", "disable_login": "Išjungti prisijungimą", - "duplicate_detection_job_description": "Vykdykite mašininį mokymąsi tam, kad aptiktumėte panašius vaizdus. Nuo šios funkcijos priklauso išmanioji paieška", + "duplicate_detection_job_description": "Vykdykite mašininį mokymąsi panašių vaizdų aptikimui. Priklauso nuo išmaniosios paieškos.", "exclusion_pattern_description": "Išimčių šablonai leidžia nepaisyti failų ir aplankų skenuojant jūsų biblioteką. Tai yra naudinga, jei turite aplankų su failais, kurių nenorite importuoti, pavyzdžiui, RAW failai.", "external_library_created_at": "Išorinė biblioteka (sukurta {date})", "external_library_management": "Išorinių bibliotekų tvarkymas", @@ -53,7 +54,7 @@ "facial_recognition_job_description": "Aptiktų veidų atpažinimas ir priskyrimas žmonėms. Šis darbas vykdomas pasibaigus \"veidų aptikimo\" darbui. \"Atstatyti\" (per)grupuoja visus aptiktus veidus. \"Trūkstami\" apdoroja jokiam žmogui dar nepriskirtus aptiktus veidus.", "failed_job_command": "Darbo {job} komanda {command} nepavyko", "force_delete_user_warning": "ĮSPĖJIMAS: Šis veiksmas iš karto pašalins naudotoją ir visą jo informaciją. Šis žingsnis nesugrąžinamas ir failų nebus galima atkurti.", - "forcing_refresh_library_files": "Priverstinai atnaujinami visi failai bilbiotekoje", + "forcing_refresh_library_files": "Priverstinai atnaujinami visi failai bibliotekoje", "image_format": "Formatas", "image_format_description": "WebP sukuria mažesnius failus nei JPEG, bet lėčiau juos apdoroja.", "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą", @@ -111,7 +112,7 @@ "machine_learning_smart_search_description": "", "machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką", "machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.", - "machine_learning_url_description": "Mašininio mokymosi serverio URL", + "machine_learning_url_description": "Mašininio mokymosi serverio URL. Jei pateikta daugiau nei vienas URL, serveriai bus bandomi eilės tvarka nuo pirmo iki paskutinio tol, kol bus rastas vienas veikiantis serveris.", "manage_concurrency": "Tvarkyti lygiagretumą", "manage_log_settings": "", "map_dark_style": "Tamsioji tema", @@ -152,20 +153,21 @@ "notification_settings": "Pranešimų nustatymai", "notification_settings_description": "Tvarkyti pranešimų nustatymus, įskaitant el. pašto", "oauth_auto_launch": "Paleisti automatiškai", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", + "oauth_auto_launch_description": "Prisijungimo puslapyje automatiškai pradėti OAuth prisijungimo procesą", + "oauth_auto_register": "Automatinis registravimas", + "oauth_auto_register_description": "Automatiškai užregistruoti naujus naudotojus po prisijungimo per OAuth", "oauth_button_text": "Mygtuko tekstas", "oauth_client_id": "Kliento ID", "oauth_client_secret": "Kliento paslaptis", "oauth_enable_description": "Prisijungti su OAuth", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", + "oauth_issuer_url": "Teikėjo URL", + "oauth_mobile_redirect_uri": "Mobiliojo peradresavimo URI", + "oauth_mobile_redirect_uri_override": "Mobiliojo peradresavimo URI pakeitimas", + "oauth_mobile_redirect_uri_override_description": "Įjunkite, kai OAuth teikėjas nepalaiko mobiliojo URI, tokio kaip '{callback}'", "oauth_scope": "", - "oauth_settings": "", + "oauth_settings": "OAuth", "oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus", + "oauth_settings_more_details": "Detaliau apie šią funkciją galite paskaityti dokumentacijoje.", "oauth_signing_algorithm": "", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", @@ -173,6 +175,7 @@ "oauth_storage_quota_claim_description": "", "oauth_storage_quota_default": "", "oauth_storage_quota_default_description": "", + "offline_paths": "Nepasiekiami adresai", "offline_paths_description": "Šie rezultatai gali būti dėl rankinio failų ištrynimo, kurie nėra išorinės bibliotekos dalis.", "password_enable_description": "Prisijungti su el. paštu ir slaptažodžiu", "password_settings": "Prisijungimas slaptažodžiu", @@ -187,13 +190,13 @@ "reset_settings_to_recent_saved": "Nustatymų atstatymas į neseniai išsaugotus nustatymus", "send_welcome_email": "Siųsti sveikinimo el. laišką", "server_external_domain_settings": "Išorinis domenas", - "server_external_domain_settings_description": "", + "server_external_domain_settings_description": "Bendrinimo nuorodų domenas, įskaitant http(s)://", "server_settings": "Serverio nustatymai", "server_settings_description": "Tvarkyti serverio nustatymus", "server_welcome_message": "Sveikinimo pranešimas", "server_welcome_message_description": "Žinutė, rodoma prisijungimo puslapyje.", "sidecar_job_description": "", - "slideshow_duration_description": "", + "slideshow_duration_description": "Sekundžių skaičius, kiek viena nuotrauka rodoma", "smart_search_job_description": "Vykdykite mašininį mokymąsi bibliotekos elementų išmaniajai paieškai", "storage_template_enable_description": "", "storage_template_hash_verification_enabled": "", @@ -269,6 +272,7 @@ "trash_settings": "Šiukšliadėžės nustatymai", "trash_settings_description": "Tvarkyti šiukšliadėžės nustatymus", "untracked_files": "Nesekami failai", + "untracked_files_description": "Šie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą", "user_delete_delay_settings": "Ištrynimo delsa", "user_delete_delay_settings_description": "", "user_management": "Naudotojų valdymas", @@ -328,7 +332,9 @@ "asset_added_to_album": "Pridėta į albumą", "asset_adding_to_album": "Pridedama į albumą...", "asset_description_updated": "Elemento aprašymas buvo atnaujintas", - "asset_offline": "", + "asset_filename_is_offline": "Elementas {filename} nepasiekiamas", + "asset_offline": "Elementas nepasiekiamas", + "asset_offline_description": "Šis išorinis elementas neberandamas diske. Dėl pagalbos susisiekite su savo Immich administratoriumi.", "asset_uploaded": "Įkelta", "asset_uploading": "Įkeliama...", "assets": "Elementai", @@ -339,6 +345,7 @@ "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementų perkelta}} į šiukšliadėžę", "assets_permanently_deleted_count": "{count, plural, one {# elementas ištrintas} few {# elementai ištrinti} other {# elementų ištrinta}} visam laikui", "assets_removed_count": "{count, plural, one {Pašalintas # elementas} few {Pašalinti # elementai} other {Pašalinta # elementų}}", + "assets_restore_confirmation": "Ar tikrai norite atkurti visus šiukšliadėžėje esančius perkeltus elementus? Šio veiksmo atšaukti negalėsite! Pastaba: nepasiekiami elementai tokiu būdu atkurti nebus.", "assets_restored_count": "{count, plural, one {Atkurtas # elementas} few {Atkurti # elementai} other {Atkurta # elementų}}", "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}} jau prieš tai buvo albume", "authorized_devices": "Autorizuoti įrenginiai", @@ -348,6 +355,9 @@ "birthdate_saved": "Sėkmingai išsaugota gimimo data", "blurred_background": "Neryškus fonas", "bugs_and_feature_requests": "Klaidų ir funkcijų užklausos", + "bulk_delete_duplicates_confirmation": "Ar tikrai norite ištrinti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Bus paliktas didžiausias kiekvienos grupės elementas ir negrįžtamai ištrinti kiti besidubliuojantys elementai. Šio veiksmo atšaukti negalėsite!", + "bulk_keep_duplicates_confirmation": "Ar tikrai norite palikti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Tokiu būdu nieko netrinant bus sutvarkytos visos dublikatų grupės.", + "bulk_trash_duplicates_confirmation": "Ar tikrai norite perkelti į šiukšliadėžę visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Bus paliktas didžiausias kiekvienos grupės elementas ir į šiukšliadėžę perkelti kiti besidubliuojantys elementai.", "buy": "Įsigyti Immich", "camera": "Fotoaparatas", "camera_brand": "Fotoaparato prekės ženklas", @@ -382,7 +392,7 @@ "comments_are_disabled": "Komentarai yra išjungti", "confirm": "Patvirtinti", "confirm_admin_password": "Patvirtinti administratoriaus slaptažodį", - "confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinamą nuorodą?", + "confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinimo nuorodą?", "confirm_password": "Patvirtinti slaptažodį", "contain": "", "context": "Kontekstas", @@ -422,6 +432,11 @@ "date_of_birth_saved": "Gimimo data sėkmingai išsaugota", "date_range": "", "day": "Diena", + "deduplicate_all": "Šalinti visus dublikatus", + "deduplication_criteria_1": "Failo dydis baitais", + "deduplication_criteria_2": "EXIF metaduomenų įrašų skaičius", + "deduplication_info": "Dublikatų šalinimo informacija", + "deduplication_info_description": "Automatinis elementų parinkimas ir masinis dublikatų šalinimas atliekamas atsižvelgiant į:", "default_locale": "", "default_locale_description": "Formatuoti datas ir skaičius pagal jūsų naršyklės lokalę", "delete": "Ištrinti", @@ -431,11 +446,11 @@ "delete_key": "Ištrinti raktą", "delete_library": "Ištrinti biblioteką", "delete_link": "Ištrinti nuorodą", - "delete_shared_link": "Ištrinti bendrinamą nuorodą", + "delete_shared_link": "Ištrinti bendrinimo nuorodą", "delete_tag": "Ištrinti žymą", "delete_tag_confirmation_prompt": "Ar tikrai norite ištrinti žymą {tagName}?", "delete_user": "Ištrinti naudotoją", - "deleted_shared_link": "Bendrinama nuoroda ištrinta", + "deleted_shared_link": "Bendrinimo nuoroda ištrinta", "description": "Aprašymas", "details": "Detalės", "direction": "Kryptis", @@ -455,6 +470,7 @@ "download_settings": "Atsisiųsti", "downloading": "Siunčiama", "duplicates": "Dublikatai", + "duplicates_description": "Sutvarkykite kiekvieną elementų grupę nurodydami elementus, kurie yra dublikatai (jei tokių yra)", "duration": "Trukmė", "edit": "Redaguoti", "edit_album": "Redaguoti albumą", @@ -492,8 +508,8 @@ "error_removing_assets_from_album": "Klaida šalinant elementus iš albumo, patikrinkite konsolę dėl išsamesnės informacijos", "exclusion_pattern_already_exists": "Šis išimčių šablonas jau egzistuoja.", "failed_to_create_album": "Nepavyko sukurti albumo", - "failed_to_create_shared_link": "Nepavyko sukurti bendrinamos nuorodos", - "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinamos nuorodos", + "failed_to_create_shared_link": "Nepavyko sukurti bendrinimo nuorodos", + "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinimo nuorodos", "failed_to_load_people": "Nepavyko užkrauti žmonių", "failed_to_remove_product_key": "Nepavyko pašalinti produkto rakto", "failed_to_stack_assets": "Nepavyko sugrupuoti elementų", @@ -503,6 +519,7 @@ "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomų pikselių. Prašome priartinti ir/arba perkelkite nuotrauką.", "quota_higher_than_disk_size": "Nustatyta kvota, viršija disko dydį", "unable_to_add_album_users": "Nepavyksta pridėti naudotojų prie albumo", + "unable_to_add_assets_to_shared_link": "Nepavyko į bendrinimo nuorodą pridėti elementų", "unable_to_add_comment": "Nepavyksta pridėti komentaro", "unable_to_add_exclusion_pattern": "Nepavyksta pridėti išimčių šablono", "unable_to_add_import_path": "Nepavyksta pridėti importavimo kelio", @@ -511,6 +528,7 @@ "unable_to_change_date": "Negalima pakeisti datos", "unable_to_change_location": "Negalima pakeisti vietos", "unable_to_change_password": "Negalima pakeisti slaptažodžio", + "unable_to_complete_oauth_login": "Nepavyko prisijungti su OAuth", "unable_to_connect": "Nepavyko prisijungti", "unable_to_connect_to_server": "Nepavyko prisijungti prie serverio", "unable_to_copy_to_clipboard": "Negalima kopijuoti į iškarpinę, įsitikinkite, kad prie puslapio prieinate per https", @@ -522,45 +540,48 @@ "unable_to_delete_asset": "", "unable_to_delete_exclusion_pattern": "Nepavyksta ištrinti išimčių šablono", "unable_to_delete_import_path": "Nepavyksta ištrinti importavimo kelio", - "unable_to_delete_shared_link": "Nepavyksta ištrinti bendrinimo nuorodos", + "unable_to_delete_shared_link": "Nepavyko ištrinti bendrinimo nuorodos", "unable_to_delete_user": "Nepavyksta ištrinti naudotojo", "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti išimčių šablono", "unable_to_edit_import_path": "Nepavyksta redaguoti išimčių kelio", "unable_to_empty_trash": "", "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano režimą", "unable_to_exit_fullscreen": "Nepavyksta išeiti iš viso ekrano režimo", - "unable_to_get_shared_link": "Nepavyksta gauti bendrinamos nuorodos", + "unable_to_get_shared_link": "Nepavyko gauti bendrinimo nuorodos", "unable_to_hide_person": "Nepavyksta paslėpti žmogaus", + "unable_to_link_oauth_account": "Nepavyko susieti su OAuth paskyra", "unable_to_load_album": "Nepavyksta užkrauti albumo", "unable_to_load_asset_activity": "", "unable_to_load_items": "", "unable_to_load_liked_status": "", "unable_to_log_out_all_devices": "Nepavyksta atjungti visų įrenginių", "unable_to_log_out_device": "Nepavyksta atjungti įrenginio", - "unable_to_login_with_oauth": "Nepavyksta prisijungti su OAuth", + "unable_to_login_with_oauth": "Nepavyko prisijungti su OAuth", "unable_to_play_video": "Nepavyksta paleisti vaizdo įrašo", "unable_to_refresh_user": "Nepavyksta atnaujinti naudotojo", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "Nepavyko pašalinti API rakto", + "unable_to_remove_assets_from_shared_link": "Nepavyko iš bendrinimo nuorodos pašalinti elementų", + "unable_to_remove_deleted_assets": "Nepavyko pašalinti nepasiekiamų elementų", "unable_to_remove_library": "Nepavyksta pašalinti bibliotekos", "unable_to_remove_partner": "Nepavyksta pašalinti partnerio", "unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos", "unable_to_repair_items": "", "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", + "unable_to_resolve_duplicate": "Nepavyko sutvarkyti dublikatų", "unable_to_restore_assets": "", "unable_to_restore_trash": "", "unable_to_restore_user": "", "unable_to_save_album": "", "unable_to_save_name": "", - "unable_to_save_profile": "", + "unable_to_save_profile": "Nepavyko išsaugoti profilio", "unable_to_save_settings": "Nepavyksta išsaugoti nustatymų", "unable_to_scan_libraries": "Nepavyksta nuskaityti bibliotekų", "unable_to_scan_library": "Nepavyksta nuskaityti bibliotekos", "unable_to_set_feature_photo": "Nepavyksta nustatyti mėgstamiausios nuotraukos", "unable_to_set_profile_picture": "Nepavyksta nustatyti profilio nuotraukos", "unable_to_submit_job": "", - "unable_to_trash_asset": "", + "unable_to_trash_asset": "Nepavyko perkelti į šiukšliadėžę", "unable_to_unlink_account": "", "unable_to_update_library": "", "unable_to_update_location": "", @@ -569,7 +590,7 @@ "unable_to_upload_file": "Nepavyksta įkelti failo" }, "exif": "Exif", - "exit_slideshow": "", + "exit_slideshow": "Išeiti iš skaidrių peržiūros", "expand_all": "Išskleisti viską", "expire_after": "", "expired": "Nebegalioja", @@ -585,6 +606,8 @@ "favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių", "favorites": "Mėgstamiausi", "feature_photo_updated": "", + "features": "Funkcijos", + "features_setting_description": "Valdyti aplikacijos funkcijas", "file_name": "Failo pavadinimas", "file_name_or_extension": "Failo pavadinimas arba plėtinys", "filename": "", @@ -592,6 +615,7 @@ "filter_people": "Filtruoti žmones", "fix_incorrect_match": "", "folders": "Aplankai", + "folders_feature_description": "Peržiūrėkite failų sistemoje esančias nuotraukas ir vaizdo įrašus aplankų rodinyje", "forward": "", "general": "", "get_help": "Gauti pagalbos", @@ -619,7 +643,7 @@ "in_archive": "Archyve", "include_archived": "Įtraukti archyvuotus", "include_shared_albums": "Įtraukti bendrinamus albumus", - "include_shared_partner_assets": "", + "include_shared_partner_assets": "Įtraukti partnerio pasidalintus elementus", "individual_share": "", "info": "Informacija", "interval": { @@ -647,8 +671,8 @@ "library_options": "Bibliotekos pasirinktys", "light": "", "link_options": "Nuorodų parinktys", - "link_to_oauth": "", - "linked_oauth_account": "", + "link_to_oauth": "Susieti su OAuth", + "linked_oauth_account": "Susieta OAuth paskyra", "list": "Sąrašas", "loading": "Kraunama", "loading_search_results_failed": "Nepavyko užkrauti paieškos rezultatų", @@ -664,13 +688,13 @@ "loop_videos": "Kartoti vaizdo įrašus", "loop_videos_description": "", "make": "Gamintojas", - "manage_shared_links": "Bendrai naudojamų nuorodų tvarkymas", + "manage_shared_links": "Bendrinimo nuorodų tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", "manage_the_app_settings": "Valdyti programos nustatymus", "manage_your_account": "Valdyti savo paskyrą", "manage_your_api_keys": "Valdyti savo API raktus", "manage_your_devices": "Valdyti prijungtus įrenginius", - "manage_your_oauth_connection": "", + "manage_your_oauth_connection": "Tvarkyti OAuth prisijungimą", "map": "Žemėlapis", "map_marker_with_image": "", "map_settings": "Žemėlapio nustatymai", @@ -710,7 +734,7 @@ "no_albums_message": "Sukurkite albumą nuotraukoms ir vaizdo įrašams tvarkyti", "no_albums_with_name_yet": "Atrodo, kad dar neturite albumų su šiuo pavadinimu.", "no_albums_yet": "Atrodo, kad dar neturite albumų.", - "no_archived_assets_message": "", + "no_archived_assets_message": "Suarchyvuokite nuotraukas ir vaizdo įrašus, kad jie nebūtų rodomi nuotraukų rodinyje", "no_assets_message": "SPUSTELĖKITE NORĖDAMI ĮKELTI PIRMĄJĄ NUOTRAUKĄ", "no_duplicates_found": "Dublikatų nerasta.", "no_exif_info_available": "", @@ -728,9 +752,10 @@ "notification_toggle_setting_description": "Įjungti el. pašto pranešimus", "notifications": "Pranešimai", "notifications_setting_description": "Tvarkyti pranešimus", - "oauth": "", + "oauth": "OAuth", "official_immich_resources": "Oficialūs Immich ištekliai", "offline": "Neprisijungęs", + "offline_paths": "Nepasiekiami adresai", "ok": "Ok", "oldest_first": "Seniausias pirmas", "onboarding_welcome_user": "Sveiki atvykę, {user}", @@ -744,7 +769,7 @@ "other": "", "other_devices": "Kiti įrenginiai", "other_variables": "Kiti kintamieji", - "owned": "", + "owned": "Nuosavi", "owner": "Savininkas", "partner": "Partneris", "partner_can_access": "{partner} gali naudotis", @@ -764,12 +789,13 @@ "path": "Kelias", "pattern": "", "pause": "Sustabdyti", - "pause_memories": "", + "pause_memories": "Pristabdyti atsiminimus", "paused": "Sustabdyta", "pending": "Laukiama", "people": "Asmenys", "people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenų}}", - "people_sidebar_description": "", + "people_feature_description": "Peržiūrėkite nuotraukas ir vaizdo įrašus sugrupuotus pagal asmenis", + "people_sidebar_description": "Rodyti asmenų rodinio nuorodą šoninėje juostoje", "permanent_deletion_warning": "", "permanent_deletion_warning_setting_description": "", "permanently_delete": "Ištrinti visam laikui", @@ -784,7 +810,7 @@ "place": "Vieta", "places": "Vietos", "play": "", - "play_memories": "", + "play_memories": "Leisti atsiminimus", "play_motion_photo": "", "play_or_pause_video": "", "port": "", @@ -794,6 +820,7 @@ "previous_memory": "", "previous_or_next_photo": "", "primary": "", + "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "Viešas albumas", "public_share": "", @@ -830,18 +857,27 @@ "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", "rating": "Įvertinimas žvaigždutėmis", "rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimų}}", + "rating_description": "Rodyti EXIF įvertinimus informacijos skydelyje", "reaction_options": "", "read_changelog": "", "recent": "", "recent_searches": "", "refresh": "Atnaujinti", + "refresh_encoded_videos": "Perkrauti apdorotus vaizdo įrašus", + "refresh_faces": "Perkrauti veidus", + "refresh_metadata": "Perkrauti metaduomenis", + "refresh_thumbnails": "Perkrauti miniatiūras", "refreshed": "Atnaujinta", - "refreshes_every_file": "", + "refreshes_every_file": "Iš naujo perskaito visus esamus ir naujai pridėtus failus", + "refreshing_encoded_video": "Perkraunamas apdorotas vaizdo įrašas", + "refreshing_faces": "Perkraunami veidai", + "refreshing_metadata": "Perkraunami metaduomenys", "remove": "Pašalinti", + "remove_assets_shared_link_confirmation": "Ar tikrai norite pašalinti {count, plural, one {# elementą} few {# elementus} other {# elementų}} iš šios bendrinimo nuorodos?", "remove_deleted_assets": "", "remove_from_album": "Pašalinti iš albumo", "remove_from_favorites": "Pašalinti iš mėgstamiausių", - "remove_from_shared_link": "", + "remove_from_shared_link": "Pašalinti iš bendrinimo nuorodos", "remove_user": "Pašalinti naudotoją", "removed_api_key": "Pašalintas API Raktas: {name}", "removed_from_archive": "Pašalinta iš archyvo", @@ -850,18 +886,19 @@ "removed_tagged_assets": "Žyma pašalinta iš {count, plural, one {# elemento} other {# elementų}}", "rename": "Pervadinti", "repair": "Pataisyti", - "repair_no_results_message": "", - "replace_with_upload": "", + "repair_no_results_message": "Nesekami ir trūkstami failai bus rodomi čia", + "replace_with_upload": "Pakeisti naujai įkeltu failu", "require_password": "Reikalauti slaptažodžio", "reset": "Atstatyti", "reset_password": "", "reset_people_visibility": "", - "resolved_all_duplicates": "Išspręsti visi dublikatai", + "resolve_duplicates": "Sutvarkyti dublikatus", + "resolved_all_duplicates": "Sutvarkyti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", "restore_user": "Atkurti naudotoją", "retry_upload": "", - "review_duplicates": "", + "review_duplicates": "Peržiūrėti dublikatus", "role": "", "save": "Išsaugoti", "saved_api_key": "Išsaugotas API raktas", @@ -898,9 +935,11 @@ "select_avatar_color": "Pasirinkti avataro spalvą", "select_face": "Pasirinkti veidą", "select_featured_photo": "Pasirinkti rodomą nuotrauką", + "select_keep_all": "Visus pažymėti \"Palikti\"", "select_library_owner": "Pasirinkti bibliotekos savininką", "select_new_face": "", "select_photos": "", + "select_trash_all": "Visus pažymėti \"Išmesti\"", "selected": "Pasirinkta", "selected_count": "{count, plural, one {# pasirinktas} few {# pasirinkti} other {# pasirinktų}}", "send_message": "Siųsti žinutę", @@ -911,28 +950,29 @@ "server_version": "Serverio versija", "set": "Nustatyti", "set_as_album_cover": "", - "set_as_profile_picture": "", + "set_as_profile_picture": "Nustatyti kaip profilio nuotrauką", "set_date_of_birth": "Nustatyti gimimo datą", "set_profile_picture": "Nustatyti profilio nuotrauką", - "set_slideshow_to_fullscreen": "", + "set_slideshow_to_fullscreen": "Nustatyti skaidrių peržiūrą per visą ekraną", "settings": "Nustatymai", "settings_saved": "", "share": "Dalintis", - "shared": "", + "shared": "Bendrinami", "shared_by": "", "shared_by_you": "", - "shared_links": "", + "shared_link_options": "Bendrinimo nuorodos parametrai", + "shared_links": "Bendrinimo nuorodos", "shared_photos_and_videos_count": "{assetCount, plural, one {# bendrinama nuotrauka ir vaizdo įrašas} few {# bendrinamos nuotraukos ir vaizdo įrašai} other {# bendrinamų nuotraukų ir vaizdo įrašų}}", "shared_with_partner": "Pasidalinta su {partner}", "sharing": "Dalijimasis", "sharing_enter_password": "Norėdami peržiūrėti šį puslapį, įveskite slaptažodį.", - "sharing_sidebar_description": "", + "sharing_sidebar_description": "Rodyti bendrinimo rodinio nuorodą šoninėje juostoje", "show_album_options": "Rodyti albumo parinktis", "show_file_location": "Rodyti rinkmenos vietą", "show_gallery": "Rodyti galeriją", "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", + "show_in_timeline": "Rodyti laiko skalėje", + "show_in_timeline_setting_description": "Rodyti šio naudotojo nuotraukas ir vaizdo įrašus mano laiko skalėje", "show_keyboard_shortcuts": "", "show_metadata": "Rodyti metaduomenis", "show_or_hide_info": "Rodyti arba slėpti informaciją", @@ -940,15 +980,18 @@ "show_person_options": "", "show_progress_bar": "", "show_search_options": "Rodyti paieškos parinktis", + "show_slideshow_transition": "Rodyti perėjimą tarp skaidrių", "show_supporter_badge": "Rėmėjo ženklelis", "show_supporter_badge_description": "Rodyti rėmėjo ženklelį", "shuffle": "", + "sidebar": "Šoninė juosta", + "sidebar_display_description": "Rodyti rodinio nuorodą šoninėje juostoje", "sign_out": "Atsijungti", "sign_up": "Užsiregistruoti", "size": "Dydis", "skip_to_content": "Pereiti prie turinio", - "slideshow": "Skaidrės", - "slideshow_settings": "", + "slideshow": "Skaidrių peržiūra", + "slideshow_settings": "Skaidrių peržiūros nustatymai", "sort_albums_by": "", "sort_created": "Sukūrimo data", "sort_modified": "Keitimo data", @@ -978,6 +1021,7 @@ "sync": "Sinchronizuoti", "tag": "Žyma", "tag_created": "Sukurta žyma: {tag}", + "tag_feature_description": "Peržiūrėkite nuotraukas ir vaizdo įrašus sugrupuotus pagal sužymėtas temas", "tag_not_found_question": "Nerandate žymos? Sukurti naują žymą.", "tag_updated": "Atnaujinta žyma: {tag}", "tagged_assets": "Žyma pridėta prie {count, plural, one {# elemento} other {# elementų}}", @@ -986,18 +1030,20 @@ "theme": "Tema", "theme_selection": "", "theme_selection_description": "", - "time_based_memories": "", + "time_based_memories": "Atsiminimai pagal laiką", + "timeline": "Laiko skalė", "timezone": "Laiko juosta", "to_archive": "Archyvuoti", "to_change_password": "Pakeisti slaptažodį", "to_favorite": "Įtraukti prie mėgstamiausių", + "to_trash": "Išmesti", "toggle_settings": "", "toggle_theme": "", "total_usage": "", "trash": "Šiukšliadėžė", - "trash_all": "Ištrinti visus", - "trash_count": "Šiukšliadėžė {count, number}", - "trash_no_results_message": "", + "trash_all": "Perkelti visus į šiukšliadėžę", + "trash_count": "Perkelti {count, number} į šiukšliadėžę", + "trash_no_results_message": "Į šiukšliadėžę perkeltos nuotraukos ir vaizdo įrašai bus rodomi čia.", "trashed_items_will_be_permanently_deleted_after": "Į šiukšliadėžę perkelti elementai bus visam laikui ištrinti po {days, plural, one {# dienos} other {# dienų}}.", "type": "Tipas", "unarchive": "Išarchyvuoti", @@ -1006,22 +1052,26 @@ "unhide_person": "", "unknown": "", "unknown_year": "Nežinomi metai", - "unlink_oauth": "", - "unlinked_oauth_account": "", + "unlink_oauth": "Atsieti OAuth", + "unlinked_oauth_account": "Atsieta OAuth paskyra", "unnamed_album_delete_confirmation": "Ar tikrai norite ištrinti šį albumą?", "unsaved_change": "Neišsaugoti pakeitimai", "unselect_all": "", "unselect_all_duplicates": "Atžymėti visus dublikatus", "unstack": "Išgrupuoti", "unstacked_assets_count": "{count, plural, one {Išgrupuotas # elementas} few {Išgrupuoti # elementai} other {Išgrupuota # elementų}}", + "untracked_files": "Nesekami failai", + "untracked_files_decription": "Šie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą", "up_next": "", "updated_password": "Slaptažodis atnaujintas", "upload": "Įkelti", "upload_concurrency": "", + "upload_errors": "Įkėlimas įvyko su {count, plural, one {# klaida} few {# klaidomis} other {# klaidų}}, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_progress": "Liko {remaining, number} - Apdorota {processed, number}/{total, number}", "upload_status_duplicates": "Dublikatai", "upload_status_errors": "Klaidos", "upload_status_uploaded": "Įkelta", + "upload_success": "Įkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "url": "URL", "usage": "", "user": "Naudotojas", @@ -1031,7 +1081,7 @@ "user_usage_stats_description": "Žiūrėti paskyros naudojimo statistiką", "username": "Naudotojo vardas", "users": "Naudotojai", - "utilities": "Priemonės", + "utilities": "Įrankiai", "validate": "Validuoti", "variables": "Kintamieji", "version": "Versija", @@ -1042,11 +1092,12 @@ "video_hover_setting_description": "Atkurti vaizdo įrašo miniatiūrą, kai pelė užvedama ant elemento. Net ir išjungus, atkūrimą galima pradėti užvedus pelės žymeklį ant atkūrimo piktogramos.", "videos": "Video", "videos_count": "{count, plural, one {# vaizdo įrašas} few {# vaizdo įrašai} other {# vaizdo įrašų}}", - "view": "Rodyti", - "view_album": "Rodyti albumą", + "view": "Žiūrėti", + "view_album": "Žiūrėti albumą", "view_all": "Peržiūrėti viską", "view_all_users": "Peržiūrėti visus naudotojus", - "view_links": "Rodyti nuorodas", + "view_in_timeline": "Žiūrėti laiko skalėje", + "view_links": "Žiūrėti nuorodas", "view_next_asset": "", "view_previous_asset": "", "view_stack": "Peržiūrėti grupę", @@ -1055,6 +1106,8 @@ "week": "Savaitė", "welcome_to_immich": "Sveiki atvykę į Immich", "year": "Metai", + "years_ago": "Prieš {years, plural, one {# metus} other {# metų}}", "yes": "Taip", + "you_dont_have_any_shared_links": "Bendrinimo nuorodų neturite", "zoom_image": "Priartinti vaizdą" } diff --git a/i18n/lv.json b/i18n/lv.json index c6cbc9c3b75a7..3d64db2b9fcca 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -35,13 +35,13 @@ "authentication_settings_reenable": "Lai atkārtoti iespējotu, izmantojiet Servera Komandu.", "background_task_job": "Fona Uzdevumi", "check_all": "Pārbaudīt Visu", - "cleared_jobs": "Notīrīti darbi priekš: {job}", + "cleared_jobs": "Notīrīti uzdevumi priekš: {job}", "config_set_by_file": "Konfigurāciju pašlaik iestata konfigurācijas fails", "confirm_delete_library": "Vai tiešām vēlaties dzēst {library} bibliotēku?", "confirm_email_below": "Lai apstiprinātu, zemāk ierakstiet “{email}”", "confirm_reprocess_all_faces": "Vai tiešām vēlaties atkārtoti apstrādāt visas sejas? Tas arī atiestatīs cilvēkus ar vārdiem.", "confirm_user_password_reset": "Vai tiešām vēlaties atiestatīt lietotāja {user} paroli?", - "create_job": "Izveidot darbu", + "create_job": "Izveidot uzdevumu", "cron_expression": "Cron izteiksme", "disable_login": "Atspējot pieteikšanos", "duplicate_detection_job_description": "Palaidiet mašīnmācīšanos uz līdzekļiem, lai noteiktu līdzīgus attēlus. Paļaujas uz Viedo Meklēšanu", @@ -59,10 +59,10 @@ "image_settings": "Attēla Iestatījumi", "image_settings_description": "Ģenerēto attēlu kvalitātes un izšķirtspējas pārvaldība", "image_thumbnail_title": "Sīktēlu iestatījumi", - "job_created": "Darbs izveidots", - "job_settings": "", - "job_settings_description": "", - "job_status": "Darbu statuss", + "job_created": "Uzdevums izveidots", + "job_settings": "Uzdevumu iestatījumi", + "job_settings_description": "Pārvaldīt uzdevumu izpildes vienlaicīgumu", + "job_status": "Uzdevumu statuss", "library_deleted": "Bibliotēka dzēsta", "library_scanning": "", "library_scanning_description": "", @@ -167,6 +167,7 @@ "repair_all": "Salabot visu", "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "scanning_library": "Skenē bibliotēku", + "search_jobs": "Meklēt uzdevumus...", "server_external_domain_settings": "", "server_external_domain_settings_description": "", "server_settings": "Servera iestatījumi", @@ -527,7 +528,7 @@ }, "invite_people": "Ielūgt cilvēkus", "invite_to_album": "Uzaicināt albumā", - "jobs": "Darbi", + "jobs": "Uzdevumi", "keep": "Paturēt", "keep_all": "Paturēt visus", "keyboard_shortcuts": "Tastatūras saīsnes", @@ -873,6 +874,8 @@ "validate": "", "variables": "", "version": "Versija", + "version_announcement_message": "Sveiki! Ir pieejama jauna Immich versija. Lūdzu, veltiet laiku, lai izlasītu laidiena piezīmes un pārliecinātos, ka jūsu iestatījumi ir atjaunināti, lai novērstu jebkādu nepareizu konfigurāciju, jo īpaši, ja izmantojat WatchTower vai citu mehānismu, kas automātiski atjaunina jūsu Immich instanci.", + "version_history": "Versiju vēsture", "video": "Videoklips", "video_hover_setting_description": "", "videos": "Videoklipi", diff --git a/i18n/ms.json b/i18n/ms.json index d35b8ce98c234..d03ef614b4a53 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -141,7 +141,7 @@ "map_implications": "Ciri peta bergantung pada perkhidmatan jubin luaran (tiles.immich.cloud)", "map_light_style": "Tema terang", "map_manage_reverse_geocoding_settings": "Urus tetapan Geocoding Songsang", - "map_reverse_geocoding": "Geocoding Terbalik", + "map_reverse_geocoding": "Geokoding Sonsang", "map_reverse_geocoding_enable_description": "Dayakan pengekodan geo terbalik", "map_reverse_geocoding_settings": "Tetapan Pengekodan Geo Terbalik", "map_settings": "Peta", @@ -229,8 +229,63 @@ "server_settings_description": "Urus tetapan pelayan", "server_welcome_message": "Mesej alu-aluan", "server_welcome_message_description": "Mesej yang dipaparkan pada halaman log masuk.", - "sidecar_job": "Metadata kereta sisi" + "sidecar_job": "Metadata kereta sisi", + "slideshow_duration_description": "Bilangan saat untuk memaparkan setiap imej", + "smart_search_job_description": "Jalankan pembelajaran mesin pada aset-aset untuk menyokong carian pintar", + "storage_template_date_time_description": "Cap masa penciptaan aset digunakan untuk maklumat masa dan tarikh", + "storage_template_date_time_sample": "Contoh masa {date}", + "storage_template_enable_description": "Dayakan enjin templat storan", + "storage_template_hash_verification_enabled": "Pengesahan hac didayakan", + "storage_template_hash_verification_enabled_description": "Mendayakan pengesahan hac, jangan lumpuhkan melainkan anda pasti akan implikasinya", + "storage_template_migration": "Penghijrahan templat storan", + "storage_template_migration_description": "Gunakan {template} semasa pada aset-aset yang dimuat naik sebelum ini", + "storage_template_migration_info": "Perubahan templat hanya akan digunakan pada aset baharu. Untuk menggunakan templat secara retroaktif pada aset-aset yang dimuat naik sebelum ini, jalankan {job}.", + "storage_template_migration_job": "Kerja Migrasi Templat Storan", + "storage_template_more_details": "Untuk butiran lanjut tentang ciri ini, rujuk kepada Templat Storan dan implikasi", + "storage_template_settings": "Templat Storan", + "theme_settings_description": "Urus penyesuaian antara muka web Immich", + "thumbnail_generation_job": "Jana Imej Kenit", + "thumbnail_generation_job_description": "Janakan imej kenit yang besar, kecil, dan kabur untuk setiap aset, serta imej kenit untuk setiap orang" }, + "deduplication_criteria_1": "Saiz imej dalam bait", + "deduplication_criteria_2": "Kiraan data EXIF", + "deduplication_info": "Maklumat Pendeduplikasian", + "deduplication_info_description": "Untuk prapilih aset secara automatik dan mengalih keluar pendua secara pukal, kami melihat pada:", + "default_locale": "Tempatan Lalai", + "delete": "Padam", + "delete_album": "Padam album", + "delete_api_key_prompt": "Adakah anda pasti mahu memadam kunci API ini?", + "delete_duplicates_confirmation": "Adakah anda pasti mahu memadam pendua ini secara kekal?", + "delete_key": "Padam kunci", + "delete_library": "Padam Pustaka", + "delete_link": "Padam pautan", + "delete_others": "Padam yang lain", + "delete_shared_link": "Padam pautan yang dikongsi", + "delete_tag": "Padam tag", + "delete_tag_confirmation_prompt": "Adakah anda pasti mahu memadam tag {tagName}?", + "delete_user": "Padam pengguna", + "deleted_shared_link": "Pautan kongsi yang dipadamkan", + "deletes_missing_assets": "Memadamkan aset yang hilang daripada cakera", + "description": "Penerangan", + "details": "Butiran", + "direction": "Arah", + "disabled": "Dilumpuhkan", + "disallow_edits": "Tolak pengeditan", + "discord": "Perselisihan", + "discover": "Terokai", + "dismiss_all_errors": "Tolak semua ralat", + "dismiss_error": "Tolak ralat", + "display_options": "Pilihan paparan", + "display_order": "Tertib paparan", + "display_original_photos": "Paparkan foto asal", + "display_original_photos_setting_description": "Mengutamakan pemaparan foto asal apabila melihat aset daripada imej kecil apabila aset asal serasi web. Ini boleh menyebabkan kelajuan paparan foto yang lebih perlahan.", + "do_not_show_again": "Jangan tunjukkan mesej ini lagi", + "documentation": "Dokumentasi", + "done": "Selesai", + "download": "Muat Turun", + "download_settings": "Muat Turun", + "download_settings_description": "Urus tetapan yang berkaitan dengan muat turun aset", + "downloading": "Memuat turun", "timeline": "Garis masa", "total": "Jumlah", "user_usage_stats": "Statistik penggunaan akaun", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 9bd4340fc8488..f0bc9d4f6de59 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -44,12 +44,14 @@ "cleared_jobs": "Ryddet opp jobber for: {job}", "config_set_by_file": "Konfigurasjonen er for øyeblikket satt av en konfigurasjonsfil", "confirm_delete_library": "Er du sikker på at du vil slette biblioteket {library}?", - "confirm_delete_library_assets": "Er du sikker på at du vil slette dette biblioteket? Dette vil slette alle {count} tilhørende eiendeler fra Immich og kan ikke angres. Filene vil forbli på disken.", + "confirm_delete_library_assets": "Er du sikker på at du vil slette dette biblioteket? Dette vil slette alle {count, plural, one {# contained asset} other {all # contained assets}} tilhørende eiendeler fra Immich og kan ikke angres. Filene vil forbli på disken.", "confirm_email_below": "For å bekrefte, skriv inn \"{email}\" nedenfor", "confirm_reprocess_all_faces": "Er du sikker på at du vil behandle alle ansikter på nytt? Dette vil også fjerne navngitte personer.", "confirm_user_password_reset": "Er du sikker på at du vil tilbakestille passordet til {user}?", "create_job": "Lag jobb", "cron_expression": "Cron uttrykk", + "cron_expression_description": "Still inn skanneintervallet med cron-formatet. For mer informasjon henvises til f.eks. Crontab Guru", + "cron_expression_presets": "Forhåndsinnstillinger for Cron-uttrykk", "disable_login": "Deaktiver innlogging", "duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Search", "exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper når du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.", @@ -67,12 +69,19 @@ "image_prefer_embedded_preview_setting_description": "Bruk innebygd forhåndsvisning i RAW-bilder som inndata til bildebehandling når tilgjengelig. Dette kan gi mer nøyaktige farger for noen bilder, men kvaliteten er avhengig av kamera og bildet kan ha komprimeringsartefakter.", "image_prefer_wide_gamut": "Foretrekk bredt fargespekter", "image_prefer_wide_gamut_setting_description": "Bruk Display P3 for miniatyrbilder. Dette bevarer glød bedre i bilder med bredt fargerom, men det kan hende bilder ser annerledes ut på gamle enheter med en gammel nettleserversjon. sRBG bilder beholdes som sRGB for å unngå fargeforskyvninger.", + "image_preview_description": "Mellomstort bilde med strippet metadata, brukt når du ser på en enkelt ressurs og for maskinlæring", + "image_preview_quality_description": "Kvalitet på forhåndsvisning fra 1-100. Høyere er bedre, men genererer større filer og kan redusere hastigheten på systemet. Ved for lav verdi kan det påvirke kvaliteten på maskinlæringen.", "image_preview_title": "Forhåndsvisningsinnstillinger", "image_quality": "Kvalitet", "image_resolution": "Oppløsning", + "image_resolution_description": "Høyere oppløsninger kan bevare flere detaljer, men det tar lengre tid å kode, har større filstørrelser og kan redusere appresponsen.", "image_settings": "Bildeinnstilliinger", "image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder", + "image_thumbnail_description": "Små miniatyrbilder med strippet metadata, brukt når du ser på grupper av bilder som hovedtidslinjen", + "image_thumbnail_quality_description": "Miniatyrbildekvalitet fra 1-100. Høyere er bedre, men produserer større filer og kan redusere appens respons.", + "image_thumbnail_title": "Miniatyrbilde oppsett", "job_concurrency": "{job} samtidighet", + "job_created": "Oppgave laget", "job_not_concurrency_safe": "Denne jobben er ikke samtidlighet sikker.", "job_settings": "Jobbinnstillinger", "job_settings_description": "Administrer parallellkjøring for jobber", @@ -129,7 +138,9 @@ "map_enable_description": "Aktiver kartfunksjoner", "map_gps_settings": "Kart & GPS Innstillinger", "map_gps_settings_description": "Administrer innstillinger for kart og GPS (Reversert geokoding)", + "map_implications": "Kartfunksjonen er avhengig av en ekstern bilde tjeneste (tiles.immich.cloud)", "map_light_style": "Lys stil", + "map_manage_reverse_geocoding_settings": "Administrer instillinger for Omvendt Geokoding", "map_reverse_geocoding": "Omvendt geokoding", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokoding", "map_reverse_geocoding_settings": "Innstillinger for omvendt geokoding", @@ -138,6 +149,8 @@ "map_style_description": "URL til et style.json-karttema", "metadata_extraction_job": "Hent metadata", "metadata_extraction_job_description": "Hent metadatainformasjon fra hver fil, for eksempel GPS-posisjon og oppløsning", + "metadata_faces_import_setting": "Aktiver ansikts importering", + "metadata_faces_import_setting_description": "Importer ansikt fra bilde EXIF data og tillegsfiler", "metadata_settings": "Metadatainnstillinger", "metadata_settings_description": "Administrer metadatainnstillinger", "migration_job": "Migrering", @@ -174,7 +187,7 @@ "oauth_issuer_url": "Utgiverens URL", "oauth_mobile_redirect_uri": "Mobil omdirigerings-URI", "oauth_mobile_redirect_uri_override": "Mobil omdirigerings-URI overstyring", - "oauth_mobile_redirect_uri_override_description": "Aktiver når 'app.immich:/' er en ugyldig omdirigerings-URI.", + "oauth_mobile_redirect_uri_override_description": "Aktiver når OAuth-leverandøren ikke tillater en mobil URI, som '{callback}'", "oauth_profile_signing_algorithm": "Profilsigneringsalgoritme", "oauth_profile_signing_algorithm_description": "Algoritme brukt for å signere brukerprofilen.", "oauth_scope": "Omfang", @@ -194,6 +207,7 @@ "password_settings": "Passordinnlogging", "password_settings_description": "Administrer innstillinger for passordinnlogging", "paths_validated_successfully": "Alle filbaner validert uten problemer", + "person_cleanup_job": "Person opprydding", "quota_size_gib": "Kvotestørrelse (GiB)", "refreshing_all_libraries": "Oppdaterer alle biblioteker", "registration": "Administrator registrering", @@ -204,9 +218,13 @@ "require_password_change_on_login": "Krev at brukeren endrer passord ved første pålogging", "reset_settings_to_default": "Tilbakestill innstillinger til standard", "reset_settings_to_recent_saved": "Tilbakestill innstillingene til de nylig lagrede innstillingene", + "scanning_library": "Søk biblioteket", + "search_jobs": "Søk etter jobber...", "send_welcome_email": "Send velkomst-e-post", "server_external_domain_settings": "Eksternt domene", "server_external_domain_settings_description": "Domene for offentlige delingslenker, inkludert http(s)://", + "server_public_users": "Offentlige brukere", + "server_public_users_description": "Alle brukere (navn og epost) blir vist når en bruker blir lagt til et delt album. Når deaktivert, vil brukerne bare bli synlig for administratorer.", "server_settings": "Serverinstillinger", "server_settings_description": "Administrer serverinnstillinger", "server_welcome_message": "Velkomstmelding", @@ -221,7 +239,7 @@ "storage_template_hash_verification_enabled": "Hash verifisering aktivert", "storage_template_hash_verification_enabled_description": "Aktiver hasjverifisering. Ikke deaktiver dette med mindre du er sikker på konsekvensene", "storage_template_migration": "Lagringsmal migrering", - "storage_template_migration_description": "Bruk gjeldende {mal} på tidligere opplastede bilder.", + "storage_template_migration_description": "Bruk gjeldende {template} på tidligere opplastede bilder", "storage_template_migration_info": "Malendringer vil kun gjelde nye ressurser. For å anvende malen på tidligere opplastede ressurser, kjør {job}.", "storage_template_migration_job": "Migreringsjobb for lagringsmal", "storage_template_more_details": "For mer informasjon om denne funksjonen, se lagringsmalen og dens konsekvenser", @@ -231,6 +249,17 @@ "storage_template_settings_description": "Administrer mappestrukturen og filnavnet til opplastede fil", "storage_template_user_label": "{label} er brukerens Lagringsetikett", "system_settings": "Systeminstillinger", + "tag_cleanup_job": "Tag opprydding", + "template_email_available_tags": "Du kan bruke følgende variabler i din mal: {tags}", + "template_email_if_empty": "Hvis malen er tom, vil standard epost bli brut.", + "template_email_invite_album": "Inviter Album Mal", + "template_email_preview": "Forhåndsvis", + "template_email_settings": "Epost mal", + "template_email_settings_description": "Administrer tilpasset mal for varslings maler", + "template_email_update_album": "Oppdater Album Mal", + "template_email_welcome": "Mal for velkomst epost", + "template_settings": "Varslings Mal", + "template_settings_description": "Administrer tilpassede maler for varsling.", "theme_custom_css_settings": "Egendefinert CSS", "theme_custom_css_settings_description": "Cascading Style Sheets gjør det mulig å tilpasse designet av Immich.", "theme_settings": "Tema innstillinger", @@ -260,6 +289,8 @@ "transcoding_constant_rate_factor": "Konstant ratefaktor (-crf)", "transcoding_constant_rate_factor_description": "Nivået på videokvaliteten. Typiske verdier er 23 for H.264, 28 for HEVC, 31 for VP9 og 35 for AV1. Lavere verdier gir bedre kvalitet, men større filstørrelser.", "transcoding_disabled_description": "Ikke transkoder noen videoer; dette kan føre til avspillingsproblemer på visse klienter", + "transcoding_encoding_options": "Kodek Alternativer", + "transcoding_encoding_options_description": "Sett kodeks, oppløsning, kvalitet og andre valg for koding av videoer", "transcoding_hardware_acceleration": "Maskinvareakselerasjon", "transcoding_hardware_acceleration_description": "Eksperimentell; mye raskere, men vil ha lavere kvalitet ved samme bithastighet", "transcoding_hardware_decoding": "Maskinvaredekoding", @@ -272,6 +303,8 @@ "transcoding_max_keyframe_interval": "Maksimal referansebilde intervall", "transcoding_max_keyframe_interval_description": "Setter maksimalt antall bilder mellom referansebilder. Lavere verdier reduserer kompresjonseffektiviteten, men forbedrer søketider og kan forbedre kvaliteten i scener med rask bevegelse. 0 setter verdien automatisk.", "transcoding_optimal_description": "Videoer som har høyere oppløsning enn målopppløsningen eller som ikke er i et akseptert format", + "transcoding_policy": "Retningslinjer for omkoding", + "transcoding_policy_description": "Velg når en video vil blir omkodet", "transcoding_preferred_hardware_device": "Foretrukken maskinvareenhet", "transcoding_preferred_hardware_device_description": "Gjelder bare for VAAPI og QSV. Angir DRI-node brukt for maskinvaretranscoding.", "transcoding_preset_preset": "Forhåndsinnstilling (-preset)", @@ -280,10 +313,10 @@ "transcoding_reference_frames_description": "Antall bilder som skal refereres til når en gitt ramme komprimeres. Høyere verdier forbedrer komprimeringseffektiviteten, men senker ned kodingen. 0 setter denne verdien automatisk.", "transcoding_required_description": "Bare videoer som ikke er i et akseptert format", "transcoding_settings": "Innstillinger for videotranskoding", - "transcoding_settings_description": "Administrer oppløsning og kodinginformasjon for videofiler", + "transcoding_settings_description": "Administrer hvilke videoer å omkode og hvordan behandle dem", "transcoding_target_resolution": "Endelig oppløsning", "transcoding_target_resolution_description": "Høyere oppløsninger kan bevare mer detaljer, men tar lengre tid å kode, resulterer i større filstørrelser, og kan redusere appens responsivitet.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "Midlertidig AQ", "transcoding_temporal_aq_description": "Gjelder kun for NVENC. Øker kvaliteten på scener med høy detaljgrad og lav bevegelse. Kan være inkompatibelt med eldre enheter.", "transcoding_threads": "Tråder", "transcoding_threads_description": "Høyere verdier fører til raskere koding, men gir mindre plass for serveren til å behandle andre oppgaver mens den er aktiv. Verdien bør ikke være mer enn antall CPU-kjerner. Maksimerer utnyttelsen hvis satt til 0.", @@ -302,6 +335,7 @@ "trash_settings_description": "Administrer papirkurv-innstillinger", "untracked_files": "Usporede filer", "untracked_files_description": "Disse filene er ikke sporet av applikasjonen. De kan være resultatet av mislykkede flyttinger, avbrutte opplastninger eller etterlatt på grunn av en feil", + "user_cleanup_job": "Bruker opprydning", "user_delete_delay": "{user}s konto og elementer vil legges i kø for permanent sletting om {delay, plural, one {# dag} other {# dager}}.", "user_delete_delay_settings": "Sletteforsinkelse", "user_delete_delay_settings_description": "Antall dager etter fjerning før en brukerkonto og dens filer permanent slettes. Brukerfjerningsjobben kjører ved midnatt for å sjekke etter brukere som er klare for sletting. Endringer i denne innstillingen vil bli evaluert ved neste utførelse.", @@ -328,10 +362,11 @@ "advanced": "Avansert", "age_months": "Alder {months, plural, one {# måned} other {# måneder}}", "age_year_months": "Alder 1 år, {months, plural, one {# måned} other {# måneder}}", + "age_years": "{years, plural, other {Age #}}", "album_added": "Album lagt til", "album_added_notification_setting_description": "Motta en e-postvarsling når du legges til i et delt album", "album_cover_updated": "Albumomslag oppdatert", - "album_delete_confirmation": "Er du sikker på at du vil slette albumet {album}?\nHvis dette albumet er delt, vil ikke andre brukere ha tilgang til det lenger.", + "album_delete_confirmation": "Er du sikker på at du vil slette albumet {album}?", "album_delete_confirmation_description": "Hvis dette albumet deles, vil andre brukere miste tilgangen til dette.", "album_info_updated": "Albuminformasjon oppdatert", "album_leave": "Forlate album?", @@ -384,16 +419,30 @@ "asset_uploading": "Laster opp...", "assets": "Filer", "assets_added_count": "Lagt til {count, plural, one {# element} other {# elementer}}", - "assets_restore_confirmation": "Er du sikker på at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres!", + "assets_added_to_album_count": "Lagt til {count, plural, one {# asset} other {# assets}} i album", + "assets_added_to_name_count": "Lagt til {count, plural, one {# asset} other {# assets}} i {hasName, select, true {{name}} other {new album}}", + "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "Flyttet {count, plural, one {# asset} other {# assets}} til søppel", + "assets_permanently_deleted_count": "Permanent slettet {count, plural, one {# asset} other {# assets}}", + "assets_removed_count": "Slettet {count, plural, one {# asset} other {# assets}}", + "assets_restore_confirmation": "Er du sikker på at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres! Vær oppmerksom på at frakoblede ressurser ikke kan gjenopprettes på denne måten.", + "assets_restored_count": "Gjenopprettet {count, plural, one {# asset} other {# assets}}", + "assets_trashed_count": "Kastet {count, plural, one {# asset} other {# assets}}", + "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} er allerede lagt til i albumet", "authorized_devices": "Autoriserte enheter", "back": "Tilbake", + "back_close_deselect": "Tilbake, lukk eller fjern merking", "backward": "Bakover", - "birthdate_saved": "Fødselsdato er lagret vellykket.", + "birthdate_saved": "Fødselsdato er vellykket lagret", "birthdate_set_description": "Fødelsdatoen er brukt for å beregne alderen til denne personen ved tidspunktet til bildet.", "blurred_background": "Uskarp bakgrunn", - "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permanent slette alle andre duplikater. Du kan ikke angre denne handlingen!", - "bulk_keep_duplicates_confirmation": "Er du sikker på at du vil beholde {count} dupliserte filer? Dette vil løse alle dupliserte grupper uten å slette noe.", - "bulk_trash_duplicates_confirmation": "Er du sikker på ønsker å slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe, samt slette alle andre duplikater.", + "bugs_and_feature_requests": "Feil og funksjonsforespørsler", + "build": "Bygg", + "build_image": "Lag Bilde", + "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count, plural, one {# duplicate asset} other {# duplicate assets}} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permanent slette alle andre duplikater. Du kan ikke angre denne handlingen!", + "bulk_keep_duplicates_confirmation": "Er du sikker på at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}} dupliserte filer? Dette vil løse alle dupliserte grupper uten å slette noe.", + "bulk_trash_duplicates_confirmation": "Er du sikker på ønsker å slette {count, plural, one {# duplicate asset} other {# duplicate assets}} dupliserte filer? Dette vil beholde største filen fra hver gruppe, samt slette alle andre duplikater.", + "buy": "Kjøp Immich", "camera": "Kamera", "camera_brand": "Kameramerke", "camera_model": "Kameramodell", @@ -420,8 +469,11 @@ "clear_all_recent_searches": "Fjern alle nylige søk", "clear_message": "Fjern melding", "clear_value": "Fjern verdi", + "clockwise": "Med urviseren", "close": "Lukk", + "collapse": "Slå sammen", "collapse_all": "Kollaps alt", + "color": "Farge", "color_theme": "Fargetema", "comment_deleted": "Kommentar slettet", "comment_options": "Kommentaralternativer", @@ -430,6 +482,7 @@ "confirm": "Bekreft", "confirm_admin_password": "Bekreft administratorpassord", "confirm_delete_shared_link": "Er du sikker på at du vil slette denne delte lenken?", + "confirm_keep_this_delete_others": "Alle andre ressurser i denne stabelen vil bli slettet bortsett fra denne ressursen. Er du sikker på at du vil fortsette?", "confirm_password": "Bekreft passord", "contain": "Inneholder", "context": "Kontekst", @@ -455,6 +508,8 @@ "create_new_person": "Opprett ny person", "create_new_person_hint": "Tildel valgte eiendeler til en ny person", "create_new_user": "Opprett ny bruker", + "create_tag": "Lag tag", + "create_tag_description": "Lag en ny tag. For undertag, vennligst fullfør hele banen til taggen, inkludert forovervendt skråstrek.", "create_user": "Opprett Bruker", "created": "Opprettet", "current_device": "Nåværende enhet", @@ -468,6 +523,10 @@ "date_range": "Datoområde", "day": "Dag", "deduplicate_all": "De-dupliser alle", + "deduplication_criteria_1": "Bilde størrelse i bytes", + "deduplication_criteria_2": "Antall av EXIF data", + "deduplication_info": "Dedupliseringsinformasjon", + "deduplication_info_description": "For å automatisk forhåndsvelge eiendeler og fjerne duplikater samtidig, ser vi på:", "default_locale": "Standard språkinnstilling", "default_locale_description": "Formater datoer og tall basert på nettleserens språkinnstilling", "delete": "Slett", @@ -477,14 +536,19 @@ "delete_key": "Slett nøkkel", "delete_library": "Slett bibliotek", "delete_link": "Slett lenke", + "delete_others": "Slett andre", "delete_shared_link": "Slett delt lenke", + "delete_tag": "Slett tag", + "delete_tag_confirmation_prompt": "Er du sikker på at du vil slette {tagName} tag?", "delete_user": "Slett bruker", "deleted_shared_link": "Slettet delt lenke", + "deletes_missing_assets": "Slett eiendeler som mangler fra disk", "description": "Beskrivelse", "details": "Detaljer", "direction": "Retning", "disabled": "Deaktivert", "disallow_edits": "Forby redigering", + "discord": "Discord", "discover": "Oppdag", "dismiss_all_errors": "Avvis alle feil", "dismiss_error": "Avvis feil", @@ -493,14 +557,20 @@ "display_original_photos": "Vis opprinnelige bilder", "display_original_photos_setting_description": "Foretrekk å vise det opprinnelige bildet når du ser på en fil i stedet for miniatyrbilder når den opprinnelige filen er kompatibel med nettet. Dette kan føre til tregere visning av bilder.", "do_not_show_again": "Ikke vis denne meldingen igjen", + "documentation": "Dokumentasjon", "done": "Ferdig", "download": "Last ned", + "download_include_embedded_motion_videos": "Innebygde videoer", + "download_include_embedded_motion_videos_description": "Inkluder innebygde videoer i levende bilder som en egen fil", "download_settings": "Last ned", "download_settings_description": "Administrer innstillinger relatert til nedlasting av filer", "downloading": "Laster ned", + "downloading_asset_filename": "Last ned {filename}", + "drop_files_to_upload": "Slipp filer hvor som helst for å laste opp", "duplicates": "Duplikater", "duplicates_description": "Løs hver gruppe ved å angi hvilke, hvis noen, er duplikater", "duration": "Varighet", + "edit": "Rediger", "edit_album": "Rediger album", "edit_avatar": "Rediger avatar", "edit_date": "Rediger dato", @@ -514,60 +584,117 @@ "edit_location": "Endre lokasjon", "edit_name": "Endre navn", "edit_people": "Rediger personer", + "edit_tag": "Rediger tag", "edit_title": "Rediger Tittel", "edit_user": "Rediger bruker", "edited": "Redigert", "editor": "Redaktør", + "editor_close_without_save_prompt": "Endringene vil ikke bli lagret", + "editor_close_without_save_title": "Lukk redigering?", + "editor_crop_tool_h2_aspect_ratios": "Sideforhold", + "editor_crop_tool_h2_rotation": "Rotasjon", "email": "E-postadresse", "empty_trash": "Tøm papirkurv", - "enable": "", - "enabled": "", + "empty_trash_confirmation": "Er du sikker på at du vil tømme søppelbøtte ? Dette vil slette alle filene i søppelbøtta permanent fra Immich.\nDu kan ikke angre denne handlingen!", + "enable": "Aktivere", + "enabled": "Aktivert", "end_date": "Slutt dato", "error": "Feil", "error_loading_image": "Feil ved lasting av bilde", + "error_title": "Feil - Noe gikk galt", "errors": { + "cannot_navigate_next_asset": "Kan ikke navigere til neste fil", + "cannot_navigate_previous_asset": "Kan ikke navigere til forrige fil", + "cant_apply_changes": "Kan ikke legge til endringene", + "cant_change_activity": "Kan ikke {enabled, select, true {disable} other {enable}} aktivitet", + "cant_change_asset_favorite": "Kan ikke endre favoritt til filen", + "cant_change_metadata_assets_count": "Kan ikke endre metadata for {count, plural, one {# asset} other {# assets}}", + "cant_get_faces": "Kan ikke finne ansikter", + "cant_get_number_of_comments": "Kan ikke hente antall kommentarer", + "cant_search_people": "Kan ikke søke etter mennesker", + "cant_search_places": "Kan ikke søke etter plasser", "cleared_jobs": "Fjernet jobber for: {job}", + "error_adding_assets_to_album": "Feil med å legge til bilder til album", + "error_adding_users_to_album": "Feil, kan ikke legge til brukere til album", + "error_deleting_shared_user": "Feil med å slette delt bruker", + "error_downloading": "Feil med å laste ned {filename}", + "error_hiding_buy_button": "Feil med å skjule kjøp knapp", + "error_removing_assets_from_album": "Feil med å fjerne bilder fra album, sjekk konsoll for mer detaljer", + "error_selecting_all_assets": "Feil med å velge alle bilder", "exclusion_pattern_already_exists": "Dette eksklusjonsmønsteret eksisterer allerede.", "failed_job_command": "Kommandoen {command} mislyktes for jobben: {job}", + "failed_to_create_album": "Feil med å lage album", + "failed_to_create_shared_link": "Feil med å lage delt lenke", + "failed_to_edit_shared_link": "Feilet med å redigere delt lenke", + "failed_to_get_people": "Feilet med å finne mennesker", + "failed_to_keep_this_delete_others": "Feilet med å beholde dette bilde og slette de andre", + "failed_to_load_asset": "Feilet med å laste bilder", + "failed_to_load_assets": "Feilet med å laste bilde", + "failed_to_load_people": "Feilen med å laste mennesker", + "failed_to_remove_product_key": "Feilet med å ta bort produkt nøkkel", + "failed_to_stack_assets": "Feilet med å stable bilder", + "failed_to_unstack_assets": "Feilet med å avstable bilder", "import_path_already_exists": "Denne importstien eksisterer allerede.", + "incorrect_email_or_password": "Feil epost eller passord", "paths_validation_failed": "{paths, plural, one {# sti} other {# sti}} mislyktes validering", + "profile_picture_transparent_pixels": "Profil bilde kan ikke ha gjennomsiktige piksler. Vennligst zoom inn og/eller flytt bilde.", "quota_higher_than_disk_size": "Du har satt en kvote høyere enn diskstørrelsen", "repair_unable_to_check_items": "Kan ikke sjekke {count, select, one {element} other {elementer}}", "unable_to_add_album_users": "Kan ikke legge til brukere i albumet", + "unable_to_add_assets_to_shared_link": "Kan ikke legge til bilder til delt lenke", "unable_to_add_comment": "Kan ikke legge til kommentar", "unable_to_add_exclusion_pattern": "Kan ikke legge til eksklusjonsmønster", "unable_to_add_import_path": "Kan ikke legge til importsti", "unable_to_add_partners": "Kan ikke legge til partnere", + "unable_to_add_remove_archive": "Kan ikke {archived, select, true {remove asset from} other {add asset to}} arkivet", + "unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {add asset to} other {remove asset from}} favoritter", + "unable_to_archive_unarchive": "Kan ikke {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "Kan ikke endre brukerens rolle i albumet", "unable_to_change_date": "Kan ikke endre dato", + "unable_to_change_favorite": "Kan ikke endre favoritt for bildet", "unable_to_change_location": "Kan ikke endre plassering", "unable_to_change_password": "Kan ikke endre passord", + "unable_to_change_visibility": "Kan ikke endre synlighet for {count, plural, one {# person} other {# people}}", + "unable_to_complete_oauth_login": "Kunne ikke fullføre OAuth innlogging", + "unable_to_connect": "Kan ikke koble til", + "unable_to_connect_to_server": "Kan ikke koble til server", "unable_to_copy_to_clipboard": "Kan ikke kopiere til utklippstavlen, sørg for at du får tilgang til siden via HTTPS", - "unable_to_create_admin_account": "", + "unable_to_create_admin_account": "Kan ikke opprette administrator bruker", "unable_to_create_api_key": "Kan ikke opprette en ny API-nøkkel", "unable_to_create_library": "Kan ikke opprette bibliotek", "unable_to_create_user": "Kan ikke opprette bruker", "unable_to_delete_album": "Kan ikke slette album", "unable_to_delete_asset": "Kan ikke slette filen", + "unable_to_delete_assets": "Feil med å slette bilde", "unable_to_delete_exclusion_pattern": "Kan ikke slette eksklusjonsmønster", "unable_to_delete_import_path": "Kan ikke slette importsti", "unable_to_delete_shared_link": "Kan ikke slette delt lenke", "unable_to_delete_user": "Kan ikke slette bruker", + "unable_to_download_files": "Kan ikke laste ned filer", "unable_to_edit_exclusion_pattern": "Kan ikke redigere eksklusjonsmønster", "unable_to_edit_import_path": "Kan ikke redigere importsti", "unable_to_empty_trash": "Kan ikke tømme papirkurven", "unable_to_enter_fullscreen": "Kan ikke gå inn i fullskjerm", "unable_to_exit_fullscreen": "Kan ikke gå ut fra fullskjerm", + "unable_to_get_comments_number": "Kan ikke hente antall kommentarer", + "unable_to_get_shared_link": "Kan ikke hente delt lenke", "unable_to_hide_person": "Kan ikke skjule person", + "unable_to_link_motion_video": "Kan ikke lenke bevegelig video", "unable_to_link_oauth_account": "Kan ikke lenke til OAuth-konto", "unable_to_load_album": "Kan ikke laste inn album", "unable_to_load_asset_activity": "Kan ikke laste inn aktivitet for filen", "unable_to_load_items": "Kan ikke laste inn elementer", "unable_to_load_liked_status": "Kan ikke laste inn likt status", + "unable_to_log_out_all_devices": "Kan ikke logge ut fra alle enheter", + "unable_to_log_out_device": "Kan ikke logge ut av enhet", + "unable_to_login_with_oauth": "Kan ikke logge inn med OAuth", "unable_to_play_video": "Kan ikke spille av video", + "unable_to_reassign_assets_existing_person": "Kunne ikke endre bruker på bildene til {name, select, null {an existing person} other {{name}}}", + "unable_to_reassign_assets_new_person": "Kunne ikke tildele bildene til en ny person", "unable_to_refresh_user": "Kan ikke oppdatere bruker", "unable_to_remove_album_users": "Kan ikke fjerne brukere fra album", "unable_to_remove_api_key": "Kan ikke fjerne API-nøkkel", + "unable_to_remove_assets_from_shared_link": "Kunne ikke fjerne bilder fra delt lenke", "unable_to_remove_deleted_assets": "Kan ikke fjerne offlinefiler", "unable_to_remove_library": "Kan ikke fjerne bibliotek", "unable_to_remove_partner": "Kan ikke fjerne partner", @@ -580,35 +707,48 @@ "unable_to_restore_user": "Kan ikke gjenopprette bruker", "unable_to_save_album": "Kan ikke lagre album", "unable_to_save_api_key": "Kan ikke lagre API-nøkkel", + "unable_to_save_date_of_birth": "Kunne ikke lagre bursdag", "unable_to_save_name": "Kan ikke lagre navn", "unable_to_save_profile": "Kan ikke lagre profil", "unable_to_save_settings": "Kan ikke lagre instillinger", "unable_to_scan_libraries": "Kan ikke skanne biblioteker", "unable_to_scan_library": "Kan ikke skanne bibliotek", + "unable_to_set_feature_photo": "Kunne ikke sette funksjonsbilde", "unable_to_set_profile_picture": "Kan ikke sette profilbilde", "unable_to_submit_job": "Kan ikke sende inn jobb", "unable_to_trash_asset": "Kan ikke flytte filen til papirkurven", "unable_to_unlink_account": "Kan ikke fjerne kobling til konto", + "unable_to_unlink_motion_video": "Kunne ikke ta på kobling på bevegelig video", + "unable_to_update_album_cover": "Kunne ikke oppdatere album bilde", + "unable_to_update_album_info": "Kunne ikke oppdatere informasjon i album", "unable_to_update_library": "Kan ikke oppdatere bibliotek", "unable_to_update_location": "Kan ikke oppdatere plassering", "unable_to_update_settings": "Kan ikke oppdatere innstillinger", "unable_to_update_timeline_display_status": "Kan ikke oppdatere visningsstatus for tidslinje", - "unable_to_update_user": "Kan ikke oppdatere bruker" + "unable_to_update_user": "Kan ikke oppdatere bruker", + "unable_to_upload_file": "Kunne ikke laste opp fil" }, + "exif": "EXIF", "exit_slideshow": "Avslutt lysbildefremvisning", "expand_all": "Utvid alle", "expire_after": "Utgå etter", "expired": "Utgått", + "expires_date": "Utløper {date}", "explore": "Utforsk", + "explorer": "Utforsker", "export": "Eksporter", "export_as_json": "Eksporter som JSON", "extension": "Utvidelse", "external": "Ekstern", "external_libraries": "Eksterne Bibliotek", + "face_unassigned": "Ikke tilordnet", + "failed_to_load_assets": "Feilet med å laste fil", "favorite": "Favoritt", "favorite_or_unfavorite_photo": "Merk som favoritt eller fjern som favoritt", "favorites": "Favoritter", "feature_photo_updated": "Fremhevet bilde oppdatert", + "features": "Funksjoner", + "features_setting_description": "Administrerer funksjoner for appen", "file_name": "Filnavn", "file_name_or_extension": "Filnavn eller filtype", "filename": "Filnavn", @@ -616,24 +756,45 @@ "filter_people": "Filtrer personer", "find_them_fast": "Finn dem raskt ved søking av navn", "fix_incorrect_match": "Fiks feilaktig match", + "folders": "Mapper", + "folders_feature_description": "Utforsker mappe visning for bilder og videoer på fil systemet", "forward": "Fremover", "general": "Generelt", "get_help": "Få Hjelp", "getting_started": "Kom i gang", "go_back": "Gå tilbake", + "go_to_folder": "Gå til mappe", "go_to_search": "Gå til søk", "group_albums_by": "Grupper album etter...", + "group_no": "Ingen gruppering", + "group_owner": "Grupper etter eiere", + "group_year": "Grupper etter år", "has_quota": "Har kvote", + "hi_user": "Hei {name} ({email})", + "hide_all_people": "Skjul alle mennesker", "hide_gallery": "Skjul galleri", + "hide_named_person": "Skjul {name}", "hide_password": "Skjul passord", "hide_person": "Skjul person", + "hide_unnamed_people": "Skjul mennesker uten navn", "host": "Vert", "hour": "Time", "image": "Bilde", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} tatt på {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} tatt med {person1} den {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} tatt med {person1} og {person2} den {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} tatt med {person1}, {person2}, og {person3} den {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tatt med {person1}, {person2}, og {additionalCount, number} andre den {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} tatt i {city}, {country} den {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} tatt i {city}, {country} med {person1} den {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tatt i {city}, {country} med {person1} og {person2} den {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tatt i {city}, {country} med {person1}, {person2}, og {person3} den {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tatt i {city}, {country} med {person1}, {person2}, ok {additionalCount, number} andre den {date}", "immich_logo": "Immich Logo", "immich_web_interface": "Immich webgrensesnitt", "import_from_json": "Importer fra JSON", "import_path": "Import-sti", + "in_albums": "I {count, plural, one {# album} other {# albums}}", "in_archive": "I arkiv", "include_archived": "Inkluder arkiverte", "include_shared_albums": "Inkluder delte album", @@ -648,19 +809,26 @@ }, "invite_people": "Inviter Personer", "invite_to_album": "Inviter til album", + "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Oppgaver", "keep": "Behold", "keep_all": "Behold alle", + "keep_this_delete_others": "Behold denne, slett de andre", + "kept_this_deleted_others": "Behold denne filen og slett {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Tastatursnarveier", "language": "Språk", "language_setting_description": "Velg ditt foretrukket språk", "last_seen": "Sist sett", + "latest_version": "Siste versjon", + "latitude": "Breddegrad", "leave": "Forlat", "let_others_respond": "La andre respondere", "level": "Nivå", "library": "Bibliotek", "library_options": "Bibliotekalternativer", "light": "Lys", + "like_deleted": "Som slettede", + "link_motion_video": "Koble bevegelsesvideo", "link_options": "Lenkealternativer", "link_to_oauth": "Lenke til OAuth", "linked_oauth_account": "Lenket til OAuth-konto", @@ -669,10 +837,17 @@ "loading_search_results_failed": "Klarte ikke å laste inn søkeresultater", "log_out": "Logg ut", "log_out_all_devices": "Logg ut fra alle enheter", + "logged_out_all_devices": "Logg ut av alle enheter", + "logged_out_device": "Logg ut enhet", + "login": "Logg inn", "login_has_been_disabled": "Login har blitt deaktivert.", + "logout_all_device_confirmation": "Er du sikker på at du vil logge ut av alle enheter?", + "logout_this_device_confirmation": "Er du sikker på at du vil logge ut av denne enheten?", + "longitude": "Lengdegrad", "look": "Se", "loop_videos": "Gjenta Videoer", "loop_videos_description": "Aktiver for å automatisk loope en video i detaljeviseren.", + "main_branch_warning": "Du bruker en utviklingsversjon; vi anbefaler på det sterkeste og bruke en utgitt versjon!", "make": "Merke", "manage_shared_links": "Håndter delte linker", "manage_sharing_with_partners": "Administrer deling med partnere", @@ -682,18 +857,22 @@ "manage_your_devices": "Administrer dine innloggede enheter", "manage_your_oauth_connection": "Administrer tilkoblingen din med OAuth", "map": "Kart", + "map_marker_for_images": "Kart makeringer for bilder tatt i {city}, {country}", "map_marker_with_image": "Kartmarkør med bilde", "map_settings": "Kartinnstillinger", "matches": "Samsvarende", "media_type": "Mediatype", "memories": "Minner", "memories_setting_description": "Administrer hva du ser i minnene dine", + "memory": "Minne", + "memory_lane_title": "Minnefelt {title}", "menu": "Meny", "merge": "Slå sammen", "merge_people": "Slå sammen personer", "merge_people_limit": "Du kan bare slå sammen opp til 5 fjes om gangen", "merge_people_prompt": "Vil du slå sammen disse personene? Denne handlingen kan ikke reverseres.", "merge_people_successfully": "Personene ble vellykket slått sammen", + "merged_people_count": "Sammenslått {count, plural, one {# person} other {# people}}", "minimize": "Minimer", "minute": "Minutt", "missing": "Mangler", @@ -705,15 +884,19 @@ "name": "Navn", "name_or_nickname": "Navn eller kallenavn", "never": "aldri", + "new_album": "Nytt Album", "new_api_key": "Ny API-nøkkel", "new_password": "Nytt passord", "new_person": "Ny person", "new_user_created": "Ny bruker opprettet", + "new_version_available": "NY VERSJON TILGJENGELIG", "newest_first": "Nyeste først", "next": "Neste", "next_memory": "Neste minne", "no": "Nei", "no_albums_message": "Opprett et album for å organisere bildene og videoene dine", + "no_albums_with_name_yet": "Det ser ut som om det ikke finnes noen album med dette navnet enda.", + "no_albums_yet": "Det ser ut som om du ikke har noen album enda.", "no_archived_assets_message": "Arkiver bilder og videoer for å skjule dem fra visningen av bildene dine", "no_assets_message": "KLIKK FOR Å LASTE OPP DITT FØRSTE BILDE", "no_duplicates_found": "Ingen duplikater ble funnet.", @@ -724,6 +907,7 @@ "no_name": "Ingen navn", "no_places": "Ingen steder", "no_results": "Ingen resultater", + "no_results_description": "Prøv et synonym eller mer generelt søkeord", "no_shared_albums_message": "Opprett et album for å dele bilder og videoer med personer i nettverket ditt", "not_in_any_album": "Ikke i noen album", "note_apply_storage_label_to_previously_uploaded assets": "Merk: For å bruke lagringsetiketten på tidligere opplastede filer, kjør", @@ -733,21 +917,32 @@ "notifications": "Notifikasjoner", "notifications_setting_description": "Administrer varsler", "oauth": "OAuth", + "official_immich_resources": "Offisielle Immich Resurser", "offline": "Frakoblet", "offline_paths": "Frakoblede stier", "offline_paths_description": "Disse resultatene kan skyldes manuell sletting av filer som ikke er en del av et eksternt bibliotek.", "ok": "Ok", "oldest_first": "Eldste først", + "onboarding": "Påmønstring", + "onboarding_privacy_description": "Følgene (valgfrie) funksjoner er avhengige av eksterne tjeneste, og kan bli deaktivert når som helst under administrator instillinger.", + "onboarding_theme_description": "Velg et fargetema for din bruker. Du kan endre denne senere under dine instillinger.", + "onboarding_welcome_description": "La oss sette opp denne installasjonen med noen vanlige instillinger.", + "onboarding_welcome_user": "Velkommen, {user}", "online": "Tilkoblet", "only_favorites": "Bare favoritter", + "open_in_map_view": "Åpne i kartvisning", + "open_in_openstreetmap": "Åpne i OpenStreetMap", "open_the_search_filters": "Åpne søkefiltrene", "options": "Valg", + "or": "eller", "organize_your_library": "Organiser biblioteket ditt", + "original": "original", "other": "Annet", "other_devices": "Andre enheter", "other_variables": "Andre variabler", "owned": "Ditt album", "owner": "Eier", + "partner": "Partner", "partner_can_access": "{partner} har tilgang", "partner_can_access_assets": "Alle bildene og videoene dine unntatt de i arkivert og slettet tilstand", "partner_can_access_location": "Stedet der bildene dine ble tatt", @@ -769,12 +964,21 @@ "paused": "Satt på pause", "pending": "Avventer", "people": "Folk", + "people_edits_count": "Endret {count, plural, one {# person} other {# people}}", + "people_feature_description": "Utforsk bilder og videoer gruppert etter mennesker", "people_sidebar_description": "Vis en lenke til Personer i sidepanelet", "permanent_deletion_warning": "Advarsel om permanent sletting", "permanent_deletion_warning_setting_description": "Vis en advarsel ved permanent sletting av filer", "permanently_delete": "Slett permanent", + "permanently_delete_assets_count": "Permanent slett {count, plural, one {asset} other {assets}}", + "permanently_delete_assets_prompt": "Er du sikker på at du vil permanent slette {count, plural, one {this asset?} other {these # assets?}} Dette vil også slette {count, plural, one {it from its} other {them from their}} album.", "permanently_deleted_asset": "Filen har blitt permanent slettet", + "permanently_deleted_assets_count": "Permanent slett {count, plural, one {# asset} other {# assets}}", + "person": "Person", + "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "photo_shared_all_users": "Det ser ut som om du deler bildene med alle brukere eller det er ingen brukere å dele med.", "photos": "Bilder", + "photos_and_videos": "Bilder & Videoer", "photos_count": "{count, plural, one {{count, number} Bilde} other {{count, number} Bilder}}", "photos_from_previous_years": "Bilder fra tidliger år", "pick_a_location": "Velg et sted", @@ -791,8 +995,31 @@ "previous_memory": "Forrige minne", "previous_or_next_photo": "Forrige eller neste bilde", "primary": "Primær", + "privacy": "Privat", + "profile_image_of_user": "Profil bilde av {user}", "profile_picture_set": "Profilbildet er satt.", + "public_album": "Offentlige album", "public_share": "Offentlig deling", + "purchase_account_info": "Støttespiller", + "purchase_activated_subtitle": "Takk for at du støtter Immich og åpen kildekode programvare", + "purchase_activated_time": "Aktiver den {date, date}", + "purchase_activated_title": "Du produktnøkkel har vellyket blitt aktivert", + "purchase_button_activate": "Aktiver", + "purchase_button_buy": "Kjøp", + "purchase_button_buy_immich": "Kjøp Immich", + "purchase_button_never_show_again": "Aldri vis igjen", + "purchase_button_reminder": "Påminn meg om 30 dager", + "purchase_button_remove_key": "Ta bort produktnøkkel", + "purchase_button_select": "Velg", + "purchase_failed_activation": "Feilet med å aktivere! Vennligst sjekk eposten for riktig produktnøkkel!", + "purchase_individual_description_1": "For en person", + "purchase_individual_description_2": "Støttespiller status", + "purchase_individual_title": "Individuell", + "purchase_input_suggestion": "Har du en produktnøkkel? Legg til denne under", + "purchase_license_subtitle": "Kjøp Immich for å støtte den videre utviklingen av systemet", + "purchase_lifetime_description": "Kjøp for livstid", + "purchase_option_title": "KJØPSVALG", + "purchase_panel_info_1": "Å lage Immich tar mye tid og energi, og nå har vi en fulltidsansatt utvikler som jobber med å gjøre produktet så godt vi kan. Vårt oppdrag er for åpen-kildekode programvare og etisk virksomhets praktisk å kunne bli bærekraftig inntekt for utviklere og for å lage privat repekterte økesystem med mulighet for å tilby skytjeneste.", "reaction_options": "Reaksjonsalternativer", "read_changelog": "Les endringslogg", "recent": "Nylig", @@ -874,7 +1101,7 @@ "shared_by_you": "Delt av deg", "shared_from_partner": "Bilder fra {partner}", "shared_links": "Delte linker", - "shared_photos_and_videos_count": "{assetCount} delte bilder og videoer.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# delte bilder og videoer.}}", "shared_with_partner": "Delt med {partner}", "sharing": "Deling", "sharing_sidebar_description": "Vis en lenke til Deling i sidepanelet", diff --git a/i18n/nl.json b/i18n/nl.json index 0627795079a43..62d87bb63e132 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -523,6 +523,10 @@ "date_range": "Datumbereik", "day": "Dag", "deduplicate_all": "Alles dedupliceren", + "deduplication_criteria_1": "Grootte van afbeelding in bytes", + "deduplication_criteria_2": "Aantal EXIF data", + "deduplication_info": "Deduplicatie-info", + "deduplication_info_description": "Om automatisch bezittingen te preselecteren en duplicaten te verwijderen in bulk, kijken we naar:", "default_locale": "Standaard landinstelling", "default_locale_description": "Formatteer datums en getallen op basis van de landinstellingen van je browser", "delete": "Verwijderen", diff --git a/i18n/pl.json b/i18n/pl.json index d118a130d17ac..49de4dc9e8534 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -523,6 +523,10 @@ "date_range": "Zakres dat", "day": "Dzień", "deduplicate_all": "Usuń Zduplikowane", + "deduplication_criteria_1": "Rozmiar obrazu w bajtach", + "deduplication_criteria_2": "Ilość plików EXIF", + "deduplication_info": "Stan duplikatów", + "deduplication_info_description": "Aby zakwalifikować elementy jako duplikaty do masowego usunięcia, sprawdzane jest:", "default_locale": "Domyślny Region", "default_locale_description": "Formatuj daty i liczby na podstawie ustawień Twojej przeglądarki", "delete": "Usuń", @@ -544,27 +548,27 @@ "direction": "Kierunek", "disabled": "Wyłączone", "disallow_edits": "Nie pozwalaj edytować", - "discord": "Discord", + "discord": "Konflikt", "discover": "Odkryj", "dismiss_all_errors": "Odrzuć wszystkie błędy", "dismiss_error": "Odrzuć błąd", "display_options": "Opcje wyświetlania", "display_order": "Kolejność wyświetlania", "display_original_photos": "Wyświetlaj oryginalne zdjęcia", - "display_original_photos_setting_description": "Wyświetlając zdjęcia i filmy, preferuj oryginalny plik zamiast miniatur jeżeli jest działa on w przeglądarce. Może to skutkować wolniejszym ładowaniem zdjęć i filmów.", + "display_original_photos_setting_description": "Wyświetlając zdjęcia i filmy, prezentuj oryginalny plik zamiast miniatur jeżeli działa on w przeglądarce. Może to skutkować wolniejszym ładowaniem zdjęć i filmów.", "do_not_show_again": "Nie pokazuj więcej tej wiadomości", "documentation": "Dokumentacja", "done": "Gotowe", "download": "Pobierz", - "download_include_embedded_motion_videos": "Osadzone filmy", + "download_include_embedded_motion_videos": "Pobierz filmy ruchomych zdjęć", "download_include_embedded_motion_videos_description": "Dołącz filmy osadzone w ruchomych zdjęciach jako oddzielny plik", "download_settings": "Pobieranie", "download_settings_description": "Zarządzaj pobieraniem zasobów", "downloading": "Pobieranie", "downloading_asset_filename": "Pobieranie zasobu {filename}", - "drop_files_to_upload": "Upuść pliki gdziekolwiek, żeby je załadować", + "drop_files_to_upload": "Upuść pliki gdziekolwiek, aby je załadować", "duplicates": "Duplikaty", - "duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby, jeśli takie istnieją, są duplikatami", + "duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby są duplikatami, jeżeli są duplikatami", "duration": "Czas trwania", "edit": "Edytuj", "edit_album": "Edytuj album", @@ -578,7 +582,7 @@ "edit_key": "Edytuj klucz", "edit_link": "Edytuj link", "edit_location": "Edytuj lokalizację", - "edit_name": "Edytuj imię", + "edit_name": "Edytuj nazwę", "edit_people": "Edytuj osoby", "edit_tag": "Edytuj etykietę", "edit_title": "Edytuj Tytuł", diff --git a/i18n/pt.json b/i18n/pt.json index 0738c647804d4..fa88c287fe8d6 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -522,7 +522,11 @@ "date_of_birth_saved": "Data de nascimento guardada com sucesso", "date_range": "Intervalo de datas", "day": "Dia", - "deduplicate_all": "Limpar todos os itens duplicados", + "deduplicate_all": "Remover todos os duplicados", + "deduplication_criteria_1": "Tamanho da imagem em bytes", + "deduplication_criteria_2": "Quantidade de dados EXIF", + "deduplication_info": "Informações sobre remoção de duplicados", + "deduplication_info_description": "Para selecionar automaticamente itens e remover duplicados em massa, vemos o seguinte:", "default_locale": "Localização Padrão", "default_locale_description": "Formatar datas e números baseados na linguagem do seu navegador", "delete": "Eliminar", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 8e7c6b5273c3f..6ad0be429bcbb 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -523,6 +523,10 @@ "date_range": "Intervalo de datas", "day": "Dia", "deduplicate_all": "Limpar todas Duplicidades", + "deduplication_criteria_1": "Tamanho do arquivo em bytes", + "deduplication_criteria_2": "Quantidade de dados EXIF", + "deduplication_info": "Informações", + "deduplication_info_description": "Ao selecionar os arquivos que serão marcados para remoção por duplicidade, será considerado os parâmetros:", "default_locale": "Localização Padrão", "default_locale_description": "Formatar datas e números baseados na linguagem do seu navegador", "delete": "Excluir", diff --git a/i18n/ru.json b/i18n/ru.json index 2beb59de60cc2..887222cb9c051 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -523,6 +523,10 @@ "date_range": "Диапазон дат", "day": "День", "deduplicate_all": "Убрать все дубликаты", + "deduplication_criteria_1": "Размер изображения в байтах", + "deduplication_criteria_2": "Подсчет данных EXIF", + "deduplication_info": "Информация о дедупликации", + "deduplication_info_description": "Для автоматического предварительного выбора объектов и массового удаления дубликатов мы рассмотрим:", "default_locale": "Дата и время по умолчанию", "default_locale_description": "Использовать формат даты и времени в соответствии с языковым стандартом вашего браузера", "delete": "Удалить", diff --git a/i18n/sk.json b/i18n/sk.json index f3610acd8e159..9af007999b677 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -5,7 +5,7 @@ "acknowledge": "Rozumiem", "action": "Akcia", "actions": "Akcie", - "active": "Aktívny", + "active": "Aktívne", "activity": "Aktivita", "activity_changed": "Aktivita je {enabled, select, true{povolená} other {zakázaná}}", "add": "Pridať", @@ -23,7 +23,7 @@ "add_to": "Pridať do...", "add_to_album": "Pridať do albumu", "add_to_shared_album": "Pridať do zdieľaného albumu", - "add_url": "Pridaj URL", + "add_url": "Pridať URL", "added_to_archive": "Pridané do archívu", "added_to_favorites": "Pridané do obľúbených", "added_to_favorites_count": "Pridané {count, number} do obľúbených", @@ -100,9 +100,9 @@ "library_watching_enable_description": "Sledovať externé knižnice pre zmeny v súboroch", "library_watching_settings": "Sledovanie knižnice (EXPERIMENTÁLNE)", "library_watching_settings_description": "Automaticky sledovať zmenené súbory", - "logging_enable_description": "Povoliť zaznamenávanie", - "logging_level_description": "Ak je povolené, akú úroveň zaznamenávania použiť.", - "logging_settings": "Zaznamenávanie", + "logging_enable_description": "Povoliť logovanie", + "logging_level_description": "Ak je povolené, akú úroveň logovania použiť.", + "logging_settings": "Logovanie", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Názov modelu CLIP je uvedený tu. Pamätajte, že pri zmene modelu je nutné znovu spustiť úlohu 'Inteligentné vyhľadávanie' pre všetky obrázky.", "machine_learning_duplicate_detection": "Detekcia duplikátov", @@ -148,7 +148,7 @@ "map_settings_description": "Spravovať nastavenia mapy", "map_style_description": "URL na motív style.json", "metadata_extraction_job": "Extrahovať metadáta", - "metadata_extraction_job_description": "Získaj informácie metadátach z každej položky, ako napríklad GPS, tváre a rozlíšenie", + "metadata_extraction_job_description": "Vytiahne metadáta z každej položky, ako napríklad GPS, tváre a rozlíšenie", "metadata_faces_import_setting": "Povoliť import tváre", "metadata_faces_import_setting_description": "Importuj tváre z EXIF dát obrázkov a sidecar súborov", "metadata_settings": "Metadáta", @@ -233,7 +233,7 @@ "sidecar_job_description": "Objavte alebo synchronizujte metadáta Sidecar zo súborového systému", "slideshow_duration_description": "Čas zobrazenia obrázku v sekundách", "smart_search_job_description": "Spustite strojové učenie na médiách na podporu inteligentného vyhľadávania", - "storage_template_date_time_description": "Časová pečiatka vytvorenia médií sa používa pre informácie o dátume a čase", + "storage_template_date_time_description": "Časová pečiatka vytvorenia položky sa používa pre informácie o dátume a čase", "storage_template_date_time_sample": "Čas vzorky {date}", "storage_template_enable_description": "Povoliť nástroj šablóny úložiska", "storage_template_hash_verification_enabled": "Overenie hash povolené", @@ -252,7 +252,7 @@ "tag_cleanup_job": "Premazanie značiek", "template_email_available_tags": "V šablóne môžeš použiť nasledujúce stítky: {tags}", "template_email_if_empty": "Ak nie je zadaná žiadna šablóna, bude použitá predvolená šablóna.", - "template_email_invite_album": "Šablóna pre Pozvánka do albumu", + "template_email_invite_album": "Šablóna Pozvánky do albumu", "template_email_preview": "Ukážka", "template_email_settings": "Emailové šablóny", "template_email_settings_description": "Spravovanie vlastných šablón pre emailové upozornenia", @@ -266,11 +266,11 @@ "theme_settings_description": "Spravovať prispôsobenie webového rozhrania Immich", "these_files_matched_by_checksum": "Tieto súbory zodpovedajú kontrolným súčtom", "thumbnail_generation_job": "Generovať Miniatúry", - "thumbnail_generation_job_description": "Generujte veľké, malé a rozmazané miniatúry pre každé médium, ako aj miniatúry pre každú osobu", + "thumbnail_generation_job_description": "Generuje veľké, malé a rozostrení miniatúry pre každú položku, ako aj miniatúry pre každú osobu", "transcoding_acceleration_api": "API pre akceleráciu", "transcoding_acceleration_api_description": "Rozhranie API, ktoré bude interagovať s vaším zariadením s cieľom urýchliť prekódovanie. Toto nastavenie je „najlepšie úsilie“: pri zlyhaní sa vráti k softvérovému prekódovaniu. VP9 môže alebo nemusí fungovať v závislosti od vášho hardvéru.", - "transcoding_acceleration_nvenc": "NVENC (vyžaduje grafickú kartu NVIDIA)", - "transcoding_acceleration_qsv": "Quick Sync (vyžaduje 7. generáciu Intel procesora alebo novšie)", + "transcoding_acceleration_nvenc": "NVENC (vyžaduje NVIDIA GPU)", + "transcoding_acceleration_qsv": "Quick Sync (vyžaduje 7. generáciu Intel CPU alebo novšiu)", "transcoding_acceleration_rkmpp": "RKMPP (iba na Rockchip SOC)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Akceptované zvukové kodeky", @@ -496,7 +496,7 @@ "copy_link_to_clipboard": "Skopírovať do schránky", "copy_password": "Skopírovať heslo", "copy_to_clipboard": "Skopírovať do schránky", - "country": "Štát", + "country": "Krajina", "cover": "Titulka", "covers": "Dlaždice", "create": "Vytvoriť", @@ -523,6 +523,10 @@ "date_range": "Rozsah dátumu", "day": "Deň", "deduplicate_all": "Deduplikovať všetko", + "deduplication_criteria_1": "Veľkosť obrázku v bajtoch", + "deduplication_criteria_2": "Počet EXIF údajov", + "deduplication_info": "Info o deduplikácii", + "deduplication_info_description": "Na automatický predvýber položiek a hromadné odstránenie duplicít, sa pozeráme do:", "default_locale": "Predvolená Lokalizácia", "default_locale_description": "Formátovanie dátumu a čísel podľa lokalizácie vášho prehliadača", "delete": "Vymazať", @@ -643,6 +647,8 @@ "unable_to_add_import_path": "Nie je možné pridať cestu importu", "unable_to_add_partners": "Nie je možné pridať partnerov", "unable_to_add_remove_archive": "Nie je možné {archived, select, true {odstrániť položku z} other {pridať položku do}} archívu", + "unable_to_add_remove_favorites": "Nepodarilo sa {favorite, select, true {pridať položku do} other {odstrániť položku z}} obľúbených", + "unable_to_archive_unarchive": "Nepodarilo sa {archived, select, true {archivovať} other {odarchivovať}}", "unable_to_change_album_user_role": "Nie je možné zmeniť rolu používateľa pre album", "unable_to_change_date": "Nie je možné zmeniť dátum", "unable_to_change_favorite": "Nie je možné zmeniť obľúbené pre položku", @@ -683,6 +689,7 @@ "unable_to_log_out_device": "Nie je možné odhlásiť zariadenie", "unable_to_login_with_oauth": "Nie je možné prihlásiť sa cez OAuth", "unable_to_play_video": "Nie je možné prehrať video", + "unable_to_reassign_assets_existing_person": "Nepodarilo sa priradiť položku k {name, select, null {existujúcej osobe} other {{name}}}", "unable_to_reassign_assets_new_person": "Nie je možné priradiť položky novej osobe", "unable_to_refresh_user": "Nie je možné aktualizovať používateľa", "unable_to_remove_album_users": "Nie je možné odstrániť používateľov z albumu", @@ -778,11 +785,11 @@ "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} nasnímané s {person1} a {person2} dňa {date}", "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} nasnímané s {person1}, {person2} a {person3} dňa {date}", "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} nasnímané s {person1}, {person2} a {additionalCount, number} inými dňa {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} nasnímané v {city}, {country} dňa {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} nasnímané v {city}, {country} s {person1} dňa {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} nasnímané v {city}, {country} s {person1} a {person2} dňa {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} nasnímané v {city}, {country} s {person1}, {person2} a {person3} dňa {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} nasnímané v {city}, {country} s {person1}, {person2} a {additionalCount, number} inými dňa {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Obrázok}} nasnímané v {city}, {country} dňa {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Obrázok}} zo dňa {date} v {city}, {country} s {person1}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Obrázok}} v {city}, {country} s {person1} a {person2} zo dňa {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Obrázok}} zo dňa {date} v {city}, {country} s {person1}, {person2} a {person3}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Obrázok}} nasnímaný v {city}, {country} s {person1}, {person2} a {additionalCount, number} inými dňa {date}", "immich_logo": "Logo Immich", "immich_web_interface": "Webové rozhranie Immich", "import_from_json": "Importovať z JSON", @@ -806,7 +813,9 @@ "jobs": "Úlohy", "keep": "Ponechať", "keep_all": "Ponechať všetko", - "keyboard_shortcuts": "", + "keep_this_delete_others": "Ponechať toto, odstrániť ostatné", + "kept_this_deleted_others": "Ponechá túto položku a odstráni {count, plural, one {# položku} other {# položiek}}", + "keyboard_shortcuts": "Klávesové skratky", "language": "Jazyk", "language_setting_description": "Vyberte preferovaný jazyk", "last_seen": "Naposledy videné", @@ -818,163 +827,264 @@ "library": "Knižnica", "library_options": "Možnosti knižnice", "light": "Svetlý", + "like_deleted": "Like odstránený", "link_motion_video": "Pripojiť pohyblivé video", "link_options": "Možnosti odkazu", "link_to_oauth": "Prepojiť s OAuth", "linked_oauth_account": "Pripojený OAuth účet", "list": "Zoznam", "loading": "Načítavanie", - "loading_search_results_failed": "", + "loading_search_results_failed": "Načítanie výsledkov hľadania sa nepodarilo", "log_out": "Odhlásiť sa", "log_out_all_devices": "Odhlásiť všetky zariadenia", "logged_out_all_devices": "Všetky zariadenia odhlásené", "logged_out_device": "Zariadenie odhlásené", "login": "Prihlásenie", "login_has_been_disabled": "Prihlásenie bolo vypnuté.", + "logout_all_device_confirmation": "Ste si istý, že sa chcete odhlásiť zo všetkých zariadení?", + "logout_this_device_confirmation": "Ste si istý, že sa chcete odhlásiť z tohoto zariadenia?", "longitude": "Zemepisná dĺžka", "look": "Zobrazenie", - "loop_videos": "", - "loop_videos_description": "", - "make": "", + "loop_videos": "Opakovať videá", + "loop_videos_description": "Povolí prehrávanie videí v slučke v detailnom zobrazení.", + "main_branch_warning": "Používate vývojársku verziu; silno odporúčame používať vydané verzie!", + "make": "Výrobca", "manage_shared_links": "Spravovať zdieľané odkazy", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", + "manage_sharing_with_partners": "Spravovať zdieľanie s partnermi", + "manage_the_app_settings": "Spravovať nastavenia aplikácie", + "manage_your_account": "Spravovať váš účet", + "manage_your_api_keys": "Spravovať vaše API kľúče", + "manage_your_devices": "Spravovať vaše prihlásené zariadenia", + "manage_your_oauth_connection": "Spravovať vaše OAuth spojenia", "map": "Mapa", - "map_marker_with_image": "", + "map_marker_for_images": "Značka na mape pre obrázky odfotené v {city}, {country}", + "map_marker_with_image": "Mapová značka pre obrázok", "map_settings": "Nastavenia máp", - "media_type": "", - "memories": "", - "memories_setting_description": "", + "matches": "Zhody", + "media_type": "Typ média", + "memories": "Spomienky", + "memories_setting_description": "Spravuje čo vidíte v spomienkach", + "memory": "Pamäť", + "memory_lane_title": "Pás spomienok {title}", "menu": "Menu", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", + "merge": "Zlúčiť", + "merge_people": "Zlúčiť ľudí", + "merge_people_limit": "Zlúčiť môžete naraz najviac 5 tvárí", + "merge_people_prompt": "Chcete zlúčiť týchto ľudí? Táto akcia sa nedá vrátiť.", + "merge_people_successfully": "Zlúčenie ľudí sa podarilo", + "merged_people_count": "Zlúčení {count, plural, one {# človek} other {# ľudia}}", + "minimize": "Minimalizovať", + "minute": "Minúta", + "missing": "Chýbajúce", + "model": "Model", "month": "Mesiac", - "more": "", - "moved_to_trash": "", - "my_albums": "", + "more": "Viac", + "moved_to_trash": "Presunuté do koša", + "my_albums": "Moje albumy", "name": "Meno", - "name_or_nickname": "", + "name_or_nickname": "Meno alebo prezývka", "never": "nikdy", - "new_api_key": "", + "new_album": "Nový album", + "new_api_key": "Nový API kľúč", "new_password": "Nové heslo", - "new_person": "", - "new_user_created": "", + "new_person": "Nová osoba", + "new_user_created": "Nový používateľ vytvorený", "new_version_available": "JE DOSTUPNÁ NOVÁ VERZIA", - "newest_first": "", + "newest_first": "Najnovšie prvé", "next": "Ďalej", - "next_memory": "", - "no": "", - "no_albums_message": "", + "next_memory": "Ďalšia spomienka", + "no": "Nie", + "no_albums_message": "Vytvorí album na organizovanie fotiek a videí", + "no_albums_with_name_yet": "Vyzerá, že zatiaľ nemáte album s týmto názvom.", + "no_albums_yet": "Vyzerá, že zatiaľ nemáte žiadne albumy.", "no_archived_assets_message": "Archivovať fotografie a videá, aby sa skryli zo zobrazenia Fotografie", - "no_assets_message": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", + "no_assets_message": "KLIKNITE A NAHRAJTE SVOJU PRVÚ FOTKU", + "no_duplicates_found": "Nenašli sa žiadne duplicity.", + "no_exif_info_available": "Nie sú dostupné exif údaje", + "no_explore_results_message": "Nahrajte viac fotiek na objavovanie vašej zbierky.", + "no_favorites_message": "Pridajte si obľúbené, aby ste rýchlo našli svoje najlepšie obrázky a videá", + "no_libraries_message": "Vytvorí externú knižnicu na prezeranie fotiek a videí", + "no_name": "Bez mena", + "no_places": "Bez miesta", + "no_results": "Žiadne výsledky", + "no_results_description": "Skúste synonymum alebo všeobecnejší výraz", + "no_shared_albums_message": "Vytvorí album na zdieľanie fotiek a videí s ľuďmi vo vašej sieti", + "not_in_any_album": "Nie je v žiadnom albume", "note_apply_storage_label_to_previously_uploaded assets": "Poznámka: Ak chcete použiť Štítok úložiska na predtým nahrané médiá, spustite príkaz", - "notes": "", + "note_unlimited_quota": "Poznámka: Zadajte 0 pre neobmedzenú kvótu", + "notes": "Poznámky", "notification_toggle_setting_description": "Povoliť e-mailové upozornenia", "notifications": "Oznámenia", "notifications_setting_description": "Spravovať upozornenia", "oauth": "OAuth", - "offline": "", - "ok": "", - "oldest_first": "", + "official_immich_resources": "Oficiálne Immich zdroje", + "offline": "Offline", + "offline_paths": "Offline cesty", + "offline_paths_description": "Tieto výsledky môžu byť kvôli ručnému vymazaniu súborov ktoré nie sú súčasťou externej knižnice.", + "ok": "OK", + "oldest_first": "Najstaršie prvé", + "onboarding": "Na palube", + "onboarding_privacy_description": "Nasledujúce (voliteľné) funkcie závisia na externých službách, a kedykoľvek ich môžete vypnúť v admin nastaveniach.", + "onboarding_theme_description": "Vyberte farbu témy pre váš server. Môžete to aj neskôr zmeniť vo vašich nastaveniach.", + "onboarding_welcome_description": "Poďme nastaviť pre váš server niekoľko základných nastavení.", "onboarding_welcome_user": "Vitaj, {user}", - "online": "", - "only_favorites": "", - "open_the_search_filters": "", + "online": "Online", + "only_favorites": "Len obľúbené", + "open_in_map_view": "Otvoriť v mape", + "open_in_openstreetmap": "Otvoriť v OpenStreetMap", + "open_the_search_filters": "Otvoriť vyhľadávacie filtre", "options": "Nastavenia", "or": "alebo", "organize_your_library": "Usporiadajte svoju knižnicu", - "other": "", + "original": "originál", + "other": "Ostatné", "other_devices": "Ďalšie zariadenia", - "other_variables": "", + "other_variables": "Ostatné premenné", "owned": "Vlastnené", "owner": "Vlastník", - "partner_sharing": "", - "partners": "", + "partner": "Partner", + "partner_can_access": "{partner} môže pristupovať", + "partner_can_access_assets": "Všetky vaše fotky a videá, okrem Archivovaných a Odstránených", + "partner_can_access_location": "Miesto kde bola fotka spravená", + "partner_sharing": "Zdieľanie s partnerom", + "partners": "Partneri", "password": "Heslo", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "Heslá sa nezhodujú", + "password_required": "Heslo je povinné", + "password_reset_success": "Obnovenie hesla úspešné", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "{days, plural, one {Posledný deň} other {Posledných # dní }}", + "hours": "{hours, plural, one {Posledná hodina} other {Posledných # hodín}}", + "years": "{years, plural, one {Posledný rok} other {Posledné # roky}}" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", + "path": "Cesta", + "pattern": "Vzor", + "pause": "Pozastaviť", + "pause_memories": "Pozastaviť spomienky", + "paused": "Pozastavené", + "pending": "Čakajúce", "people": "Ľudia", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", + "people_edits_count": "{count, plural, one {Upravená # osoba} other {Upravených # ľudí}}", + "people_feature_description": "Prehliadanie fotiek a videí zoskupených podľa ľudí", + "people_sidebar_description": "Zobrazí odkaz na Ľudí v bočnom paneli", + "permanent_deletion_warning": "Varovanie o trvalom zmazaní", + "permanent_deletion_warning_setting_description": "Zobraziť varovanie pri trvalom zmazaní položky", + "permanently_delete": "Trvalo zmazať", + "permanently_delete_assets_count": "Navždy zmazať {count, plural, one {položku} other {položky}}", + "permanently_delete_assets_prompt": "Naozaj si prajete navždy zmazať {count, plural, one {túto položku?} other {týchto # položiek?}} Vymažú sa aj {count, plural, one {zo svojho albumu} other {zo svojich albumov}}.", + "permanently_deleted_asset": "Navždy odstránená položka", + "permanently_deleted_assets_count": "Navždy {count, plural, one {odstránená # položka} other {odstránené # položky}}", + "person": "Osoba", + "person_hidden": "{name}{hidden, select, true { (skryté)} other {}}", + "photo_shared_all_users": "Vyzerá, že zdieľate svoje fotky so všetkými používateľmi alebo nemáte žiadnych používateľov.", "photos": "Fotografie", "photos_and_videos": "Fotografie & Videa", - "photos_from_previous_years": "", - "pick_a_location": "", + "photos_count": "{count, plural, one {{count, number} Fotka} other {{count, number} Fotiek}}", + "photos_from_previous_years": "Fotky z minulých rokov", + "pick_a_location": "Vyberte miesto", "place": "Miesto", "places": "Miesta", "play": "Prehrať", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", + "play_memories": "Prehrať spomienky", + "play_motion_photo": "Prehrať pohyblivú fotku", + "play_or_pause_video": "Pustí alebo pozastaví video", + "port": "Port", + "preset": "Prednastavenie", + "preview": "Náhľad", + "previous": "Predošlé", + "previous_memory": "Predošlá spomienka", + "previous_or_next_photo": "Predošlá alebo ďalšia fotka", + "primary": "Primárne", + "privacy": "Súkromie", + "profile_image_of_user": "Profilový obrázok používateľa {user}", + "profile_picture_set": "Profilový obrázok nastavený.", "public_album": "Verejný album", - "public_share": "", + "public_share": "Verejné zdieľanie", + "purchase_account_info": "Podporovateľ", + "purchase_activated_subtitle": "Ďakujeme za podporu Immich a softvéru s otvorenými zdrojákmi", "purchase_activated_time": "Aktivované {date, date}", + "purchase_activated_title": "Váš kľúč je úspešne aktivovaný", "purchase_button_activate": "Aktivovať", + "purchase_button_buy": "Kúpiť", + "purchase_button_buy_immich": "Kúpiť Immich", "purchase_button_never_show_again": "Už viac nezobrazovať", + "purchase_button_reminder": "Pripomenúť mi o 30 dní", + "purchase_button_remove_key": "Odobrať kľúč", + "purchase_button_select": "Vybrať", + "purchase_failed_activation": "Aktivácia sa nepodarila! Prosím skontrolujte email či je správny kľúč produktu!", + "purchase_individual_description_1": "Pre jednotlivca", + "purchase_individual_description_2": "Stav podporovateľa", + "purchase_individual_title": "Jednotlivec", + "purchase_input_suggestion": "Máte produktový kľúč? Zadajte ho nižšie", + "purchase_license_subtitle": "Kúpte si Immich a podporte neustály vývoj tejto služby", + "purchase_lifetime_description": "Doživotná platnosť", + "purchase_option_title": "MOŽNOSTI NÁKUPU", + "purchase_panel_info_1": "Vývoj Immich zaberá veľa času a úsilia, a máme zamestnaných fulltime inžinierov, aby ho spravili ako sa najlepšie dá. Naša misia je, aby sa open-source softvér a etické biznis praktiky stali udržateľným zdrojom príjmu pre vývojárov a vytvorili ekosystém rešpektujúci súkromie so skutočnými náhradami voči zneužívajúcim cloudovým službám.", + "purchase_panel_info_2": "Keďže sme zaviazaní nezavádzať paywally, nezískate týmto nákupom žiadne prídavné funkcie. Spoliehame sa na používateľov ako vy na podporu neustáleho vývoja Immich.", "purchase_panel_title": "Podporiť projekt", - "reaction_options": "", - "read_changelog": "", + "purchase_per_server": "Za server", + "purchase_per_user": "Za používateľa", + "purchase_remove_product_key": "Odstrániť produktový kľúč", + "purchase_remove_product_key_prompt": "Naozaj chcete odstrániť produktový kľúč?", + "purchase_remove_server_product_key": "Odstrániť produktový kľúč servera", + "purchase_remove_server_product_key_prompt": "Naozaj chcete odstrániť produktový kľúč servera?", + "purchase_server_description_1": "Pre celý server", + "purchase_server_description_2": "Stav podporovateľa", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Produktový kľúč servera spravuje admin", + "rating": "Hodnotenie hviezdičkami", + "rating_clear": "Vyčistiť hodnotenie", + "rating_count": "{count, plural, one {# hviezdička} other {# hviezdičky}}", + "rating_description": "Zobrazí EXIF hodnotenie v info paneli", + "reaction_options": "Možnosti reakcie", + "read_changelog": "Prečítať zoznam zmien", + "reassign": "Preradiť", + "reassigned_assets_to_existing_person": "Preradené {count, plural, one {# položka} other {# položky}} k {name, select, null {existujúcej osobe} other {{name}}}", + "reassigned_assets_to_new_person": "Preradené {count, plural, one {# položka} other {# položiek}} novej osobe", + "reassing_hint": "Priradí zvolenú položku k existujúcej osobe", "recent": "Nedávne", - "recent_searches": "", + "recent-albums": "Posledné albumy", + "recent_searches": "Posledné vyhľadávania", "refresh": "Obnoviť", + "refresh_encoded_videos": "Obnoviť enkódované videá", + "refresh_faces": "Obnoviť tváre", "refresh_metadata": "Obnoviť metadáta", "refresh_thumbnails": "Obnoviť miniatúry", "refreshed": "Aktualizované", - "refreshes_every_file": "", + "refreshes_every_file": "Znova prečíta všetky existujúce a nové súbory", + "refreshing_encoded_video": "Obnovovanie enkódovaných videí", + "refreshing_faces": "Obnovovnie tvárí", + "refreshing_metadata": "Obnovovanie metadát", + "regenerating_thumbnails": "Pregenerovanie náhľadov", "remove": "Odstrániť", - "remove_deleted_assets": "", + "remove_assets_album_confirmation": "Naozaj chcete odstrániť {count, plural, one {# položky} other {# položiek}} z albumu?", + "remove_assets_shared_link_confirmation": "Naozaj chcete odstrániť {count, plural, one {# položku} other {# položiek}} z tohoto zdieľaného odkazu?", + "remove_assets_title": "Odstrániť položky?", + "remove_custom_date_range": "Odstrániť vlastný rozsah dátumov", + "remove_deleted_assets": "Odstrániť vymazané položky", "remove_from_album": "Odstrániť z albumu", - "remove_from_favorites": "", - "remove_from_shared_link": "", + "remove_from_favorites": "Odstrániť z obľúbených", + "remove_from_shared_link": "Odstrániť zo zdieľaného odkazu", + "remove_url": "Odstrániť URL", "remove_user": "Odstrániť používateľa", + "removed_api_key": "Odstrániť API kľúč: {name}", + "removed_from_archive": "Odstránené z archívu", + "removed_from_favorites": "Odstránené z obľúbených", + "removed_from_favorites_count": "{count, plural, other {Odstránených #}} z obľúbených", + "removed_tagged_assets": "Odstránená značka z {count, plural, one {# položky} other {# položiek}}", + "rename": "Premenovať", "repair": "Opraviť", - "repair_no_results_message": "", - "replace_with_upload": "", + "repair_no_results_message": "Nesledované a chýbajúce súbory sa zobrazia tu", + "replace_with_upload": "Nahradiť nahraním", + "repository": "Repozitár", "require_password": "Vyžadovať heslo", + "require_user_to_change_password_on_first_login": "Vyžadovať zmenu hesla po prvom prihlásení", "reset": "Resetovať", "reset_password": "Obnoviť heslo", - "reset_people_visibility": "", + "reset_people_visibility": "Resetovať viditeľnosť ľudí", + "reset_to_default": "Resetovať na predvolené", + "resolve_duplicates": "Vyriešiť duplicity", + "resolved_all_duplicates": "Vyriešené všetky duplicity", "restore": "Navrátiť", "restore_all": "Navrátit všetko", "restore_user": "Navrátiť používateľa", @@ -1017,7 +1127,7 @@ "search_your_photos": "Hľadajte svoje fotky", "searching_locales": "Hľadám lokality...", "second": "Sekundy", - "see_all_people": "Vydieť všetky osoby", + "see_all_people": "Pozrieť všetky osoby", "select_album_cover": "Vyberte obal albumu", "select_all": "Vybrať všetko", "select_all_duplicates": "Vybrať všetky duplikáty", @@ -1040,6 +1150,7 @@ "server_version": "Verzia Servera", "set": "Nastaviť", "set_as_album_cover": "Nastaviť ako obal albumu", + "set_as_featured_photo": "Nastaviť ako hlavnú fotku", "set_as_profile_picture": "Nastaviť ako profilový obrázok", "set_date_of_birth": "Nastaviť dátum narodenia", "set_profile_picture": "Nastaviť profilový obrázok", @@ -1062,91 +1173,143 @@ "shift_to_permanent_delete": "stlačte ⇧ pre nemenné zmazanie pložiek", "show_album_options": "Zobraziť možnosti albumu", "show_albums": "Zobraziť albumy", - "show_file_location": "", + "show_all_people": "Zobraziť všetkých ľudí", + "show_and_hide_people": "Zobraziť a skryť ľudí", + "show_file_location": "Zobrazí umiestnenie súboru", "show_gallery": "Zobraziť galériu", - "show_hidden_people": "", + "show_hidden_people": "Zobraziť skrytých ľudí", "show_in_timeline": "Zobraziť na časovej osi", - "show_in_timeline_setting_description": "", + "show_in_timeline_setting_description": "Zobrazí fotky a videá tohoto používateľa na časovej osi", "show_keyboard_shortcuts": "Zobraziť klávesové skratky", "show_metadata": "Zobraziť metadáta", - "show_or_hide_info": "", + "show_or_hide_info": "Zobrazí alebo skryje info", "show_password": "Zobraziť heslo", - "show_person_options": "", - "show_progress_bar": "", + "show_person_options": "Zobrazí možnosti osoby", + "show_progress_bar": "Zobrazí ukazovateľ priebehu", "show_search_options": "Zobraziť možnosti vyhľadávania", - "shuffle": "", + "show_slideshow_transition": "Zobrazí prechody v prezentácii", + "show_supporter_badge": "Odznak podporovateľa", + "show_supporter_badge_description": "Zobraziť odznak podporovateľa", + "shuffle": "Náhodné poradie", + "sidebar": "Bočný panel", + "sidebar_display_description": "Zobrazí odkaz na pohľad v bočnom paneli", "sign_out": "Odhlásiť sa", - "sign_up": "", + "sign_up": "Registrovať", "size": "Veľkosť", - "skip_to_content": "", + "skip_to_content": "Preskočiť na obsah", + "skip_to_folders": "Preskočiť do priečinkov", "skip_to_tags": "Preskočiť ku štítkom", - "slideshow": "", - "slideshow_settings": "", + "slideshow": "Prezentácia", + "slideshow_settings": "Nastavenia prezentácie", "sort_albums_by": "Zoradiť albumy podľa...", "sort_created": "Dátum vytvorenia", "sort_items": "Počet položiek", "sort_modified": "Dátum úpravy", "sort_oldest": "Najstaršia fotografia", + "sort_people_by_similarity": "Zoradiť ľudí podľa podobnosti", "sort_recent": "Najnovšia fotografia", "sort_title": "Názov", "source": "Zdroj", "stack": "Zoskupenie", - "stack_selected_photos": "", - "stacktrace": "", + "stack_duplicates": "Zoskupiť duplicity", + "stack_select_one_photo": "Vyberte jednu hlavnú fotku pre zoskupenie", + "stack_selected_photos": "Zoskupiť vybraté fotky", + "stacked_assets_count": "{count, plural, one {Zoskupená # položka} other {Zoskupených # položiek}}", + "stacktrace": "Výpis zásobníku", + "start": "Štart", "start_date": "Začiatočný dátum", - "state": "", - "status": "", - "stop_motion_photo": "", + "state": "Stav", + "status": "Stav", + "stop_motion_photo": "Stopmotion fotka", "stop_photo_sharing": "Zastaviť zdieľanie vašich fotiek?", + "stop_photo_sharing_description": "{partner} už nebude mať prístup k vašim fotkám.", + "stop_sharing_photos_with_user": "Zastaviť zdieľanie týchto fotiek s týmto používateľom", "storage": "Ukladací priestor", "storage_label": "Štítok úložiska", + "storage_usage": "Využitých {used} z {available}", "submit": "Odoslať", "suggestions": "Návrhy", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", + "sunrise_on_the_beach": "Východ slnka na pláži", + "support": "Podpora", + "support_and_feedback": "Podpora a spätná väzba", + "support_third_party_description": "Vaša inštalácia Immich bola pripravená treťou stranou. Problémy, ktoré sa vyskytli, môžu byť spôsobené týmto balíčkom, preto sa na nich obráťte v prvom rade cez nasledujúce odkazy.", + "swap_merge_direction": "Vymeniť smer zlúčenia", + "sync": "Synchronizovať", + "tag": "Značka", + "tag_assets": "Pridať značku", + "tag_created": "Vytvorená značka: {tag}", + "tag_feature_description": "Prehliadanie fotiek a videá zoskupených podľa tematických značiek", + "tag_not_found_question": "Neviete nájsť značku? Vytvorte novú značku.", + "tag_updated": "Upravená značka: {tag}", + "tagged_assets": "Značka priradená {count, plural, one {# položke} other {# položkám}}", "tags": "Štítky", - "template": "", + "template": "Šablóna", "theme": "Téma", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", + "theme_selection": "Výber témy", + "theme_selection_description": "Automaticky nastaví tému na svetlú alebo tmavú podľa systémových preferencií v prehliadači", + "they_will_be_merged_together": "Zlúčia sa dokopy", + "third_party_resources": "Zdroje tretích strán", + "time_based_memories": "Časové spomienky", + "timeline": "Časová os", "timezone": "Časové pásmo", "to_archive": "Archivovať", "to_change_password": "Zmeniť heslo", + "to_favorite": "Obľúbiť", + "to_login": "Prihlásiť", + "to_parent": "Prejsť k nadradenému", "to_trash": "Kôš", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", + "toggle_settings": "Prepnúť nastavenie", + "toggle_theme": "Prepnúť tmavú tému", + "total": "Celkom", + "total_usage": "Celkové využitie", "trash": "Kôš", - "trash_all": "", + "trash_all": "Všetko do koša", + "trash_count": "{count, number} do koša", + "trash_delete_asset": "Položky do koša/odstrániť", "trash_no_results_message": "Vymazané fotografie a videá sa zobrazia tu.", - "type": "", + "trashed_items_will_be_permanently_deleted_after": "Položky v koši sa natrvalo vymažú po {days, plural, one {# dni} other {# dňoch}}.", + "type": "Typ", "unarchive": "Odarchivovať", + "unarchived_count": "{count, plural, other {Odarchivovaných #}}", "unfavorite": "Odznačiť ako obľúbené", - "unhide_person": "", - "unknown": "", + "unhide_person": "Odkryť osobu", + "unknown": "Neznáme", "unknown_year": "Neznámy rok", - "unlink_oauth": "", - "unlinked_oauth_account": "", + "unlimited": "Neobmedzené", + "unlink_motion_video": "Odpojiť pohyblivé video", + "unlink_oauth": "Odpojiť OAuth", + "unlinked_oauth_account": "Odpojiť OAuth účet", + "unnamed_album": "Nepomenovaný album", "unnamed_album_delete_confirmation": "Ste si istý, že chcete zmazať tento album?", + "unnamed_share": "Nepomenované zdieľanie", "unsaved_change": "Neuložená zmena", - "unselect_all": "", + "unselect_all": "Zrušiť výber všetkých", + "unselect_all_duplicates": "Zrušiť výber všetkých duplicít", "unstack": "Odskupiť", - "up_next": "", - "updated_password": "", + "unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}", + "untracked_files": "Nesledované súbory", + "untracked_files_decription": "Tieto súbory nie sú sledované aplikáciou. Dôvodom môže byť zlyhaný presun, prerušené nahrávanie, alebo výsledkom bugu", + "up_next": "To je všetko", + "updated_password": "Heslo zmenené", "upload": "Nahrať", - "upload_concurrency": "", + "upload_concurrency": "Súbežnosť nahrávania", + "upload_errors": "Nahrávanie ukončené s {count, plural, one {# chybou} other {# chybami}}, obnovte stránku aby sa zobrazili nové položky.", + "upload_progress": "Ostáva {remaining, number} - Spracovaných {processed, number}/{total, number}", + "upload_skipped_duplicates": "{count, plural, one {Preskočená # duplicita} few {Preskočené # duplicity} other {Preskočených # duplicít}}", "upload_status_duplicates": "Duplikáty", "upload_status_errors": "Chyby", "upload_status_uploaded": "Nahrané", "upload_success": "Nahrávanie úspešné, pridané súbory sa zobrazia po obnovení stránky.", "url": "Odkaz URL", "usage": "Použitie", + "use_custom_date_range": "Použite radšej vlastný rozsah dátumov", "user": "Používateľ", "user_id": "Používateľské ID", + "user_liked": "Používateľovi {user} sa páči {type, select, photo {táto fotka} video {toto video} asset {táto položka} other {toto}}", + "user_purchase_settings": "Nákup", + "user_purchase_settings_description": "Správa vášho nákupu", "user_role_set": "Nastav {user} ako {role}", - "user_usage_detail": "", + "user_usage_detail": "Podrobnosti o využívaní používateľmi", "user_usage_stats": "Štatistiky využitia účtu", "user_usage_stats_description": "Zobraziť štatistiky využitia účtu", "username": "Používateľské meno", @@ -1156,24 +1319,32 @@ "variables": "Premenné", "version": "Verzia", "version_announcement_closing": "Tvoj kamarát, Alex", + "version_announcement_message": "Ahoj! Nová verzia Immich je dostupná. Prosím prečítajte si poznámky k vydaniu, aby ste sa uistili, že inštalácia bude aktuálna bez problémov, najmä ak používate WatchTower alebo akýkoľvek spôsob automatickej aktualizácie Immich servera.", "version_history": "História verzií", + "version_history_item": "Inštalovaná {version} dňa {date}", "video": "Video", - "video_hover_setting_description": "", + "video_hover_setting": "Prehrávať video náhľad pri nabehnutí myšou", + "video_hover_setting_description": "Prehrá video náhľad keď kurzor myši prejde cez položku. Aj keď je vypnuté, prehrávanie sa môže spustiť nabehnutí cez ikonu Prehrať.", "videos": "Videá", + "videos_count": "{count, plural, one {# Video} few {# Videá} other {# Videí}}", "view": "Zobraziť", "view_album": "Zobraziť Album", "view_all": "Zobraziť všetky", "view_all_users": "Zobraziť všetkých používateľov", "view_in_timeline": "Zobraziť v časovej osi", "view_links": "Zobraziť odkazy", + "view_name": "Zobraziť", "view_next_asset": "Zobraziť nasledujúci súbor", "view_previous_asset": "Zobraziť predchádzajúci súbor", - "waiting": "", + "view_stack": "Zobraziť zoskupenie", + "visibility_changed": "Viditeľnosť zmenená pre {count, plural, one {# osobu} other {# ľudí}}", + "waiting": "Čaká", "warning": "Varovanie", "week": "Týždeň", "welcome": "Vitajte", "welcome_to_immich": "Vitajte v Immich", "year": "Rok", + "years_ago": "pred {years, plural, one {# rokom} other {# rokmi}}", "yes": "Áno", "you_dont_have_any_shared_links": "Nemáte žiadne zdielané linky", "zoom_image": "Priblížiť obrázok" diff --git a/i18n/sl.json b/i18n/sl.json index 6f26af2563c13..5073efcabc069 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -523,6 +523,10 @@ "date_range": "Časovno obdobje", "day": "Dan", "deduplicate_all": "Odstrani vse podvojene", + "deduplication_criteria_1": "Velikost slike v bajtih", + "deduplication_criteria_2": "Število podatkov EXIF", + "deduplication_info": "Informacije o deduplikaciji", + "deduplication_info_description": "Za samodejno vnaprejšnjo izbiro sredstev in množično odstranjevanje dvojnikov si ogledamo:", "default_locale": "Privzeti jezik", "default_locale_description": "Oblikujte datume in številke glede na lokalne nastavitve brskalnika", "delete": "Izbriši", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index 6d7ba24a24e29..d9b095467ba60 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -523,6 +523,10 @@ "date_range": "Распон датума", "day": "Дан", "deduplicate_all": "Де-дуплицирај све", + "deduplication_criteria_1": "Величина слике у бајтовима", + "deduplication_criteria_2": "Број EXIF података", + "deduplication_info": "Информације о дедупликацији", + "deduplication_info_description": "Да бисмо аутоматски унапред одабрали датотеке и уклонили дупликате групно, гледамо:", "default_locale": "Подразумевана локација (locale)", "default_locale_description": "Форматирајте датуме и бројеве на основу локализације вашег претраживача", "delete": "Обриши", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index 13bc7f11779df..42536767683be 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -523,6 +523,10 @@ "date_range": "Raspon datuma", "day": "Dan", "deduplicate_all": "De-dupliciraj sve", + "deduplication_criteria_1": "Veličina slike u bajtovima", + "deduplication_criteria_2": "Broj EXIF podataka", + "deduplication_info": "Informacije o deduplikaciji", + "deduplication_info_description": "Da bismo automatski unapred odabrali datoteke i uklonili duplikate grupno, gledamo:", "default_locale": "Podrazumevana lokacija (locale)", "default_locale_description": "Formatirajte datume i brojeve na osnovu lokalizacije vašeg pretraživača", "delete": "Obriši", diff --git a/i18n/sv.json b/i18n/sv.json index 6910fa95894c1..73d9ff51cbc2f 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -523,8 +523,11 @@ "date_range": "Datumintervall", "day": "Dag", "deduplicate_all": "Deduplicera alla", + "deduplication_criteria_1": "Bildstorlek i bytes", + "deduplication_criteria_2": "Räkning av EXIF-data", + "deduplication_info": "Dedupliceringsinformation", "default_locale": "Standardplats", - "default_locale_description": "Formatera datum och siffror baserat på din webbläsares lokalitet", + "default_locale_description": "Formatera datum och siffror baserat på din webbläsares språkversion", "delete": "Radera", "delete_album": "Ta bort album", "delete_api_key_prompt": "Är du säker på att du vill ta bort denna API-nyckel?", @@ -913,6 +916,7 @@ "notifications_setting_description": "Hantera aviseringar", "oauth": "OAuth", "offline": "Frånkopplad", + "offline_paths": "Offlinevägar", "offline_paths_description": "Dessa resultat kan bero på att filer som ej ingår i ett externt bibliotek har tagits bort manuellt.", "ok": "OK", "oldest_first": "Äldst först", @@ -957,7 +961,7 @@ "pending": "Väntande", "people": "Personer", "people_edits_count": "Redigerad {count, plural, one {# person} other {# people}}", - "people_feature_description": "Visar foton och videor grupperade per personer", + "people_feature_description": "Visar foton och videor grupperade efter personer", "people_sidebar_description": "Visa en länk till Personer i sidopanelen", "permanent_deletion_warning": "Varning om permanent radering", "permanent_deletion_warning_setting_description": "Visa en varning när tillgångar raderas permanent", diff --git a/i18n/ta.json b/i18n/ta.json index f3ed200b24c42..c3d13dbdf0bc0 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -7,7 +7,7 @@ "actions": "செயல்கள்", "active": "செயல்பாட்டில்", "activity": "செயல்பாடுகள்", - "activity_changed": "செயல்பாடு {இயக்கப்பட்டது, தேர்ந்தெடு, சரி {இயக்கப்பட்டது} மற்றது {முடக்கப்பட்டது}}", + "activity_changed": "செயல்பாடு {இயக்கப்பட்டது, தேர்ந்தெடு, சரி {enabled} மற்றது {disabled}}", "add": "சேர்", "add_a_description": "விவரம் சேர்", "add_a_location": "இடத்தை சேர்க்கவும்", diff --git a/i18n/uk.json b/i18n/uk.json index dbc14f2e2cb85..773b9b7c73733 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -438,7 +438,7 @@ "blurred_background": "Розмитий фон", "bugs_and_feature_requests": "Помилки та Запити", "build": "Збірка", - "build_image": "Створити зображення", + "build_image": "Версія збірки", "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", @@ -523,6 +523,10 @@ "date_range": "Проміжок часу", "day": "День", "deduplicate_all": "Видалити всі дублікати", + "deduplication_criteria_1": "Розмір зображення в байтах", + "deduplication_criteria_2": "Кількість даних EXIF", + "deduplication_info": "Інформація про дедуплікацію", + "deduplication_info_description": "Для автоматичного попереднього вибору файлів і масового видалення дублікатів ми враховуємо:", "default_locale": "Дата і час за замовчуванням", "default_locale_description": "Форматувати дати та числа з урахуванням мови вашого браузера", "delete": "Видалити", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 12599f385ad63..12c72a8172834 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -523,6 +523,10 @@ "date_range": "日期范围", "day": "日", "deduplicate_all": "删除所有重复项", + "deduplication_criteria_1": "图像大小(字节)", + "deduplication_criteria_2": "EXIF 数据计数", + "deduplication_info": "重复数据删除汇总", + "deduplication_info_description": "要自动预选项目并批量删除重复项,我们会考虑:", "default_locale": "默认地区", "default_locale_description": "根据您的浏览器地区设置日期和数字显示格式", "delete": "删除", From fc99c5f53096f2c19f8fe0921c59a909c88a90cb Mon Sep 17 00:00:00 2001 From: Yonathan Randolph Date: Mon, 13 Jan 2025 19:00:55 -0800 Subject: [PATCH 033/184] chore(server): avoid copying sources in dev (#12794) * chore(server): avoid copying sources in dev Add a dev target to the web and server Dockerfiles, and change docker-compose.dev.yml to use the dev target. The dev target avoids copying files so that the docker image is smaller. * chore: respond to PR: don't add dev target web/Dockerfile is only used by docker-compose.dev.yml so a dev target is redundant. Instead, just remove the copy --------- Co-authored-by: Jason Rasmussen --- server/Dockerfile | 2 +- web/Dockerfile | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index e4fb6d43521eb..85c3ffae1f242 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -9,7 +9,6 @@ RUN npm ci && \ # they're marked as optional dependencies, so we need to copy them manually after pruning rm -rf node_modules/@img/sharp-libvips* && \ rm -rf node_modules/@img/sharp-linuxmusl-x64 -COPY server . ENV PATH="${PATH}:/usr/src/app/bin" \ IMMICH_ENV=development \ NVIDIA_DRIVER_CAPABILITIES=all \ @@ -19,6 +18,7 @@ ENTRYPOINT ["tini", "--", "/bin/sh"] FROM dev AS prod +COPY server . RUN npm run build RUN npm prune --omit=dev --omit=optional COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img diff --git a/web/Dockerfile b/web/Dockerfile index bf6aa5af5c2b7..dfef1d83481d3 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -5,7 +5,6 @@ USER node WORKDIR /usr/src/app COPY --chown=node:node package*.json ./ RUN npm ci -COPY --chown=node:node . . ENV CHOKIDAR_USEPOLLING=true EXPOSE 24678 EXPOSE 3000 From a35af2b24248a242ef8732379ba6fce1410794cb Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jan 2025 22:22:03 -0600 Subject: [PATCH 034/184] refactor: migrate move repository to kysely (#15327) * refactor: migrate move repository to kysely * fix: tests * fix: tests --- server/src/cores/storage.core.ts | 4 +- server/src/interfaces/move.interface.ts | 10 +++-- server/src/queries/move.repository.sql | 29 ++++++------ server/src/repositories/move.repository.ts | 44 ++++++++++++++----- .../services/storage-template.service.spec.ts | 4 +- 5 files changed, 56 insertions(+), 35 deletions(-) diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index c49175172d66e..d26829d633550 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -183,7 +183,7 @@ export class StorageCore { return; } - move = await this.moveRepository.update({ id: move.id, oldPath: actualPath, newPath }); + move = await this.moveRepository.update(move.id, { id: move.id, oldPath: actualPath, newPath }); } else { move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath }); } @@ -225,7 +225,7 @@ export class StorageCore { } await this.savePath(pathType, entityId, newPath); - await this.moveRepository.delete(move); + await this.moveRepository.delete(move.id); } private async verifyNewPathContentsMatchesExpected( diff --git a/server/src/interfaces/move.interface.ts b/server/src/interfaces/move.interface.ts index 0e79cfcadc5a8..4356d9df8cc8d 100644 --- a/server/src/interfaces/move.interface.ts +++ b/server/src/interfaces/move.interface.ts @@ -1,3 +1,5 @@ +import { Insertable, Updateable } from 'kysely'; +import { MoveHistory } from 'src/db'; import { MoveEntity } from 'src/entities/move.entity'; import { PathType } from 'src/enum'; @@ -6,8 +8,8 @@ export const IMoveRepository = 'IMoveRepository'; export type MoveCreate = Pick & Partial; export interface IMoveRepository { - create(entity: MoveCreate): Promise; - getByEntity(entityId: string, pathType: PathType): Promise; - update(entity: Partial): Promise; - delete(move: MoveEntity): Promise; + create(entity: Insertable): Promise; + getByEntity(entityId: string, pathType: PathType): Promise; + update(id: string, entity: Updateable): Promise; + delete(id: string): Promise; } diff --git a/server/src/queries/move.repository.sql b/server/src/queries/move.repository.sql index 3ce8c0ccddeb4..e51f2829df49e 100644 --- a/server/src/queries/move.repository.sql +++ b/server/src/queries/move.repository.sql @@ -1,18 +1,17 @@ -- NOTE: This file is auto generated by ./sql-generator -- MoveRepository.getByEntity -SELECT - "MoveEntity"."id" AS "MoveEntity_id", - "MoveEntity"."entityId" AS "MoveEntity_entityId", - "MoveEntity"."pathType" AS "MoveEntity_pathType", - "MoveEntity"."oldPath" AS "MoveEntity_oldPath", - "MoveEntity"."newPath" AS "MoveEntity_newPath" -FROM - "move_history" "MoveEntity" -WHERE - ( - ("MoveEntity"."entityId" = $1) - AND ("MoveEntity"."pathType" = $2) - ) -LIMIT - 1 +select + * +from + "move_history" +where + "entityId" = $1 + and "pathType" = $2 + +-- MoveRepository.delete +delete from "move_history" +where + "id" = $1 +returning + * diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index 16d90040145b8..c0177f3f30f1a 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -1,29 +1,49 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, MoveHistory } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MoveEntity } from 'src/entities/move.entity'; import { PathType } from 'src/enum'; -import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface'; -import { Repository } from 'typeorm'; +import { IMoveRepository } from 'src/interfaces/move.interface'; @Injectable() export class MoveRepository implements IMoveRepository { - constructor(@InjectRepository(MoveEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} - create(entity: MoveCreate): Promise { - return this.repository.save(entity); + create(entity: Insertable): Promise { + return this.db + .insertInto('move_history') + .values(entity) + .returningAll() + .executeTakeFirstOrThrow() as Promise; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - getByEntity(entityId: string, pathType: PathType): Promise { - return this.repository.findOne({ where: { entityId, pathType } }); + getByEntity(entityId: string, pathType: PathType): Promise { + return this.db + .selectFrom('move_history') + .selectAll() + .where('entityId', '=', entityId) + .where('pathType', '=', pathType) + .executeTakeFirst() as Promise; } - update(entity: Partial): Promise { - return this.repository.save(entity); + update(id: string, entity: Updateable): Promise { + return this.db + .updateTable('move_history') + .set(entity) + .where('id', '=', id) + .returningAll() + .executeTakeFirstOrThrow() as unknown as Promise; } - delete(move: MoveEntity): Promise { - return this.repository.remove(move); + @GenerateSql({ params: [DummyValue.UUID] }) + delete(id: string): Promise { + return this.db + .deleteFrom('move_history') + .where('id', '=', id) + .returningAll() + .executeTakeFirstOrThrow() as unknown as Promise; } } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 728e891d05379..46ec4f53e1531 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -235,7 +235,7 @@ describe(StorageTemplateService.name, () => { expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3); expect(storageMock.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); - expect(moveMock.update).toHaveBeenCalledWith({ + expect(moveMock.update).toHaveBeenCalledWith('123', { id: '123', oldPath: assetStub.image.originalPath, newPath, @@ -277,7 +277,7 @@ describe(StorageTemplateService.name, () => { expect(storageMock.stat).toHaveBeenCalledWith(previousFailedNewPath); expect(storageMock.rename).toHaveBeenCalledWith(previousFailedNewPath, newPath); expect(storageMock.copyFile).not.toHaveBeenCalled(); - expect(moveMock.update).toHaveBeenCalledWith({ + expect(moveMock.update).toHaveBeenCalledWith('123', { id: '123', oldPath: previousFailedNewPath, newPath, From 9e1651ef6662e5fd0cc2ceea0b62bacb45f9724c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 13 Jan 2025 23:40:19 -0500 Subject: [PATCH 035/184] fix: bump web dependencies (#15325) --- web/package-lock.json | 972 ++++++++++++++++++++---------------------- web/package.json | 58 +-- 2 files changed, 481 insertions(+), 549 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 9450b76834318..426df0acd78d6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,17 +9,17 @@ "version": "1.124.2", "license": "GNU Affero General Public License version 3", "dependencies": { - "@formatjs/icu-messageformat-parser": "^2.7.8", + "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", - "@photo-sphere-viewer/core": "^5.7.1", - "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", - "@photo-sphere-viewer/video-plugin": "^5.7.2", + "@photo-sphere-viewer/core": "^5.11.5", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", + "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", - "intl-messageformat": "^10.5.14", + "intl-messageformat": "^10.7.11", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", @@ -32,43 +32,43 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.8.0", - "@faker-js/faker": "^9.0.0", + "@eslint/js": "^9.18.0", + "@faker-js/faker": "^9.3.0", "@socket.io/component-emitter": "^3.1.0", - "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/enhanced-img": "^0.4.0", - "@sveltejs/kit": "^2.12.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/enhanced-img": "^0.4.4", + "@sveltejs/kit": "^2.15.2", + "@sveltejs/vite-plugin-svelte": "^4.0.4", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/svelte": "^5.2.4", + "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.5.2", "@types/dom-to-image": "^2.6.7", "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "@vitest/coverage-v8": "^2.0.5", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", + "@vitest/coverage-v8": "^2.1.8", "autoprefixer": "^10.4.17", - "dotenv": "^16.4.5", - "eslint": "^9.14.0", + "dotenv": "^16.4.7", + "eslint": "^9.18.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.45.1", + "eslint-plugin-svelte": "^2.46.1", "eslint-plugin-unicorn": "^56.0.1", "factory.ts": "^1.4.1", - "globals": "^15.9.0", - "postcss": "^8.4.35", - "prettier": "^3.2.5", + "globals": "^15.14.0", + "postcss": "^8.5.0", + "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", - "prettier-plugin-sort-json": "^4.0.0", - "prettier-plugin-svelte": "^3.2.6", - "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^5.1.5", - "svelte-check": "^4.0.9", - "tailwindcss": "^3.4.1", + "prettier-plugin-sort-json": "^4.1.1", + "prettier-plugin-svelte": "^3.3.3", + "rollup-plugin-visualizer": "^5.14.0", + "svelte": "^5.17.4", + "svelte-check": "^4.1.4", + "tailwindcss": "^3.4.17", "tslib": "^2.6.2", - "typescript": "^5.5.0", - "vite": "^5.4.4", + "typescript": "^5.7.3", + "vite": "^5.4.11", "vitest": "^2.0.5" } }, @@ -220,10 +220,11 @@ "dev": true }, "node_modules/@emnapi/runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", - "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -646,13 +647,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -661,11 +662,14 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -739,9 +743,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", "dev": true, "license": "MIT", "engines": { @@ -749,9 +753,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -759,12 +763,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -772,9 +777,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.2.0.tgz", - "integrity": "sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", "dev": true, "funding": [ { @@ -789,50 +794,51 @@ } }, "node_modules/@formatjs/ecma402-abstract": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.3.tgz", - "integrity": "sha512-aElGmleuReGnk2wtYOzYFmNWYoiWWmf1pPPCYg0oiIQSJj0mjc4eUfzUXaSOJ4S8WzI/cLqnCTWjqz904FT2OQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", + "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==", "license": "MIT", "dependencies": { - "@formatjs/fast-memoize": "2.2.3", - "@formatjs/intl-localematcher": "0.5.7", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.5.10", + "decimal.js": "10", "tslib": "2" } }, "node_modules/@formatjs/fast-memoize": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", - "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", "license": "MIT", "dependencies": { "tslib": "2" } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.3.tgz", - "integrity": "sha512-9L99QsH14XjOCIp4TmbT8wxuffJxGK8uLNO1zNhLtcZaVXvv626N0s4A2qgRCKG3dfYWx9psvGlFmvyVBa6u/w==", + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.8.tgz", + "integrity": "sha512-hZlLNI3+Lev8IAXuwehLoN7QTKqbx3XXwFW1jh0AdIA9XJdzn9Uzr+2LLBspPm/PX0+NLIfykj/8IKxQqHUcUQ==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.2.3", - "@formatjs/icu-skeleton-parser": "1.8.7", + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/icu-skeleton-parser": "1.8.12", "tslib": "2" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.7.tgz", - "integrity": "sha512-fI+6SmS2g7h3srfAKSWa5dwreU5zNEfon2uFo99OToiLF6yxGE+WikvFSbsvMAYkscucvVmTYNlWlaDPp0n5HA==", + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz", + "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.2.3", + "@formatjs/ecma402-abstract": "2.3.2", "tslib": "2" } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.7.tgz", - "integrity": "sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", "license": "MIT", "dependencies": { "tslib": "2" @@ -890,450 +896,380 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", - "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", - "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", - "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", - "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", - "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", - "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", - "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", - "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", - "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.0" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", - "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", - "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1638,30 +1574,31 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.11.1.tgz", - "integrity": "sha512-bxWnoQGYjXfmHGee4OSkoYLZmdgqvJWMn7wmpK0V0Vf46Fqu+TJ4Yt8+dY2PgpM89HoKzNr15Dzt6jqOfjkFxQ==", + "version": "5.11.5", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.11.5.tgz", + "integrity": "sha512-aCo+zsWR0m0qSlNQpkacnQoSfc0An0zujBpQJ5l9LSvZixeC85FTWm9OZs1yaXvE5bM+TsdqfPDojjy9xT8qzQ==", "license": "MIT", "dependencies": { "three": "^0.169.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.11.1.tgz", - "integrity": "sha512-fkWuVeArtZSWd0z282/J82YSc+oernQaE/cpo0soVaStaNbS1V35iSnPlaBKw40qX6tucJWYw15QwM8xgPC2IQ==", + "version": "5.11.5", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.11.5.tgz", + "integrity": "sha512-OmB5lYtJCHAbOI06X5KABILsjfLhmWp17uEvS9FQrHWX5cYjsSn+T2flBfgxqrVi0gXFSYmh80yoF3tgWjoc9Q==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.11.1" + "@photo-sphere-viewer/core": "5.11.5", + "@photo-sphere-viewer/video-plugin": "5.11.5" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.1.tgz", - "integrity": "sha512-02spWwv9bjyI6inNdZsczX/qdMICVV9B8lWX/J4iNBaiUCHqPKmk8CeZbRyC/Uh3OHSusSJHyW0FDEOf6qjjww==", + "version": "5.11.5", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.5.tgz", + "integrity": "sha512-Jlbx01y3HGwCVlaXPzeMl/LQ2Vqy9LLO9qxmBGoX/aHAse9QsMatl1N1+EUcDZcC4rZcCsNja9OxRN2r/hfnDA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.11.1" + "@photo-sphere-viewer/core": "5.11.5" } }, "node_modules/@pkgjs/parseargs": { @@ -1980,9 +1917,9 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz", - "integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz", + "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1990,13 +1927,14 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.4.1.tgz", - "integrity": "sha512-Z0xwQWM7tfdlNYuaFsAsbjEosEZb961yP7hlvZBLlh3+Rv4tI3BboD6bUkmInj+cC66p/5rybgvEtxX5LILSuw==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.4.4.tgz", + "integrity": "sha512-BlBTGfbLUgHa+zSVrsGLOd+noCKWfipoOjoxE26bAAX97v7zh5eiCAp1KEdpkluL05Tl3+nR14gQdPsATyZqoA==", "dev": true, "license": "MIT", "dependencies": { "magic-string": "^0.30.5", + "sharp": "^0.33.5", "svelte-parse-markup": "^0.1.5", "vite-imagetools": "^7.0.1", "zimmerframe": "^1.1.2" @@ -2007,9 +1945,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.13.0.tgz", - "integrity": "sha512-6t6ne00vZx/TjD6s0Jvwt8wRLKBwbSAN1nhlOzcLUSTYX1hTp4eCBaTPB5Yz/lu+tYcvz4YPEEuPv3yfsNp2gw==", + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.2.tgz", + "integrity": "sha512-p208T1kdM6zd8k4YXIUM60pLWQ8dZqehXSiqn4NulXHyHibX53uIAL2xtNL8GjxX2IVPqPRT978MwVYhCKExdQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2040,9 +1978,9 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.1.tgz", - "integrity": "sha512-prXoAE/GleD2C4pKgHa9vkdjpzdYwCSw/kmjw6adIyu0vk5YKCfqIztkLg10m+kOYnzZu3bb0NaPTxlWre2a9Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", "dev": true, "license": "MIT", "dependencies": { @@ -2271,10 +2209,11 @@ } }, "node_modules/@testing-library/svelte": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.4.tgz", - "integrity": "sha512-EFdy73+lULQgMJ1WolAymrxWWrPv9DWyDuDFKKlUip2PA/EXuHptzfYOKWljccFWDKhhGOu3dqNmoc2f/h/Ecg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.6.tgz", + "integrity": "sha512-1Y8cEg/BtV4J6g9irkY0ksz+ueDFYLiikjTLiqvQPkOUeDzR4gg2zECBf8yrOrCy3e2TAOYMcaysFa0bQMyk1w==", "dev": true, + "license": "MIT", "dependencies": { "@testing-library/dom": "^10.0.0" }, @@ -2430,21 +2369,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", + "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/type-utils": "8.20.0", + "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2455,25 +2394,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", + "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4" }, "engines": { @@ -2484,23 +2419,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2511,16 +2442,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", + "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/utils": "8.20.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2530,18 +2461,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", "dev": true, "license": "MIT", "engines": { @@ -2553,20 +2480,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2575,10 +2502,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2608,16 +2533,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2627,22 +2552,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.20.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2667,9 +2588,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz", - "integrity": "sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", + "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", "dev": true, "license": "MIT", "dependencies": { @@ -2690,8 +2611,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.5", - "vitest": "2.1.5" + "@vitest/browser": "2.1.8", + "vitest": "2.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2700,14 +2621,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", - "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, @@ -2716,13 +2637,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", - "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", + "@vitest/spy": "2.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, @@ -2743,9 +2664,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", - "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2756,13 +2677,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", - "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.5", + "@vitest/utils": "2.1.8", "pathe": "^1.1.2" }, "funding": { @@ -2770,13 +2691,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", - "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.8", "magic-string": "^0.30.12", "pathe": "^1.1.2" }, @@ -2785,9 +2706,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", - "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", "dev": true, "license": "MIT", "dependencies": { @@ -2798,13 +2719,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", - "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.8", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -3346,6 +3267,15 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -3577,10 +3507,7 @@ "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true, - "optional": true, - "peer": true + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/deep-eql": { "version": "5.0.2", @@ -3674,10 +3601,11 @@ "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -3755,9 +3683,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -3868,27 +3796,27 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", + "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.18.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -3907,8 +3835,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -3949,6 +3876,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3957,9 +3885,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.0.tgz", - "integrity": "sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==", + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", "dev": true, "license": "MIT", "dependencies": { @@ -4066,16 +3994,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", @@ -4278,13 +4196,12 @@ } }, "node_modules/esrap": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", - "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.2.tgz", + "integrity": "sha512-FhVlJzvTw7ZLxYZ7RyHwQCFE64dkkpzGNNnphaGCLwjqGk1SQcqzbgdx9FowPCktx6NOSHkzvcZ3vsvdH54YXA==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/esrecurse": { @@ -4313,6 +4230,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -4689,9 +4607,9 @@ } }, "node_modules/globals": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", - "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", "dev": true, "license": "MIT", "engines": { @@ -4943,14 +4861,14 @@ } }, "node_modules/intl-messageformat": { - "version": "10.7.6", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.6.tgz", - "integrity": "sha512-IsMU/hqyy3FJwNJ0hxDfY2heJ7MteSuFvcnCebxRp67di4Fhx1gKKE+qS0bBwUF8yXkX9SsPUhLeX/B6h5SKUA==", + "version": "10.7.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz", + "integrity": "sha512-IB2N1tmI24k2EFH3PWjU7ivJsnWyLwOWOva0jnXFa29WzB6fb0JZ5EMQGu+XN5lDtjHYFo0/UooP67zBwUg7rQ==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/ecma402-abstract": "2.2.3", - "@formatjs/fast-memoize": "2.2.3", - "@formatjs/icu-messageformat-parser": "2.9.3", + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.9.8", "tslib": "2" } }, @@ -6110,9 +6028,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.0.tgz", + "integrity": "sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==", "dev": true, "funding": [ { @@ -6130,7 +6048,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6307,9 +6225,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -6340,10 +6258,11 @@ } }, "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.1.1.tgz", + "integrity": "sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.0.0" }, @@ -6352,9 +6271,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.8.tgz", - "integrity": "sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6714,13 +6633,14 @@ } }, "node_modules/rollup-plugin-visualizer": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", - "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz", + "integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==", "dev": true, + "license": "MIT", "dependencies": { "open": "^8.4.0", - "picomatch": "^2.3.1", + "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, @@ -6728,17 +6648,34 @@ "rollup-plugin-visualizer": "dist/bin/cli.js" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { + "rolldown": "1.x", "rollup": "2.x || 3.x || 4.x" }, "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, "rollup": { "optional": true } } }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/rollup-plugin-visualizer/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -6863,43 +6800,43 @@ } }, "node_modules/sharp": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", - "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.3", - "@img/sharp-darwin-x64": "0.33.3", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.3", - "@img/sharp-linux-arm64": "0.33.3", - "@img/sharp-linux-s390x": "0.33.3", - "@img/sharp-linux-x64": "0.33.3", - "@img/sharp-linuxmusl-arm64": "0.33.3", - "@img/sharp-linuxmusl-x64": "0.33.3", - "@img/sharp-wasm32": "0.33.3", - "@img/sharp-win32-ia32": "0.33.3", - "@img/sharp-win32-x64": "0.33.3" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -7271,9 +7208,9 @@ } }, "node_modules/svelte": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.1.tgz", - "integrity": "sha512-WzyA7VUVlDTLPt+m71bLD5BXasavqvAo68DelxWaPo8dNEZ3tmeq3DSJPsWqnG37cG2hfn7HaD3x882qF+7UOw==", + "version": "5.17.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.17.4.tgz", + "integrity": "sha512-ne4IhhVBwzpUByjo1ocxQnqRoWsRilc9Ry1j+0uPWhHmg4jS/nnlSwYYfx7Ium8okCZ4hYM89rg0B5G0hQzk+g==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -7283,8 +7220,9 @@ "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", - "esm-env": "^1.0.0", - "esrap": "^1.2.2", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -7295,9 +7233,9 @@ } }, "node_modules/svelte-check": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.9.tgz", - "integrity": "sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.4.tgz", + "integrity": "sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==", "dev": true, "license": "MIT", "dependencies": { @@ -7407,9 +7345,9 @@ } }, "node_modules/svelte-gestures": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.6.tgz", - "integrity": "sha512-kElJnoZrQtlkXE0O/RcKioz9NP0Sxx05j31ohyosNkydo6NOEsZB85mhoaCxOQNjxN+QPumYWfmIUsznYFjihA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.4.tgz", + "integrity": "sha512-a6cnR46AfFZ8zZyvA38A1wBLBFI7rYuAWQnmv3yYgSdbaJK/U7JG34rSkjMCePRvf4BETJSDfMNngLs5zEAfbw==", "license": "MIT" }, "node_modules/svelte-i18n": { @@ -7921,9 +7859,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", - "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", "dependencies": { @@ -7936,7 +7874,7 @@ "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", - "lilconfig": "^2.1.0", + "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", @@ -7958,6 +7896,19 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -7994,19 +7945,6 @@ } } }, - "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/tailwindcss/node_modules/yaml": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", @@ -8058,13 +7996,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8217,15 +8148,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -8258,9 +8190,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8463,9 +8395,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", - "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", "dev": true, "license": "MIT", "dependencies": { @@ -8505,19 +8437,19 @@ } }, "node_modules/vitest": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", - "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.5", - "@vitest/mocker": "2.1.5", - "@vitest/pretty-format": "^2.1.5", - "@vitest/runner": "2.1.5", - "@vitest/snapshot": "2.1.5", - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", @@ -8529,7 +8461,7 @@ "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.5", + "vite-node": "2.1.8", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8544,8 +8476,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.5", - "@vitest/ui": "2.1.5", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", "happy-dom": "*", "jsdom": "*" }, diff --git a/web/package.json b/web/package.json index 7ceae0a0ae220..b843f6c13af8d 100644 --- a/web/package.json +++ b/web/package.json @@ -24,58 +24,58 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.8.0", - "@faker-js/faker": "^9.0.0", + "@eslint/js": "^9.18.0", + "@faker-js/faker": "^9.3.0", "@socket.io/component-emitter": "^3.1.0", - "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/enhanced-img": "^0.4.0", - "@sveltejs/kit": "^2.12.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/enhanced-img": "^0.4.4", + "@sveltejs/kit": "^2.15.2", + "@sveltejs/vite-plugin-svelte": "^4.0.4", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/svelte": "^5.2.4", + "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.5.2", "@types/dom-to-image": "^2.6.7", "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "@vitest/coverage-v8": "^2.0.5", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", + "@vitest/coverage-v8": "^2.1.8", "autoprefixer": "^10.4.17", - "dotenv": "^16.4.5", - "eslint": "^9.14.0", + "dotenv": "^16.4.7", + "eslint": "^9.18.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.45.1", + "eslint-plugin-svelte": "^2.46.1", "eslint-plugin-unicorn": "^56.0.1", "factory.ts": "^1.4.1", - "globals": "^15.9.0", - "postcss": "^8.4.35", - "prettier": "^3.2.5", + "globals": "^15.14.0", + "postcss": "^8.5.0", + "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", - "prettier-plugin-sort-json": "^4.0.0", - "prettier-plugin-svelte": "^3.2.6", - "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^5.1.5", - "svelte-check": "^4.0.9", - "tailwindcss": "^3.4.1", + "prettier-plugin-sort-json": "^4.1.1", + "prettier-plugin-svelte": "^3.3.3", + "rollup-plugin-visualizer": "^5.14.0", + "svelte": "^5.17.4", + "svelte-check": "^4.1.4", + "tailwindcss": "^3.4.17", "tslib": "^2.6.2", - "typescript": "^5.5.0", - "vite": "^5.4.4", + "typescript": "^5.7.3", + "vite": "^5.4.11", "vitest": "^2.0.5" }, "type": "module", "dependencies": { - "@formatjs/icu-messageformat-parser": "^2.7.8", + "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", - "@photo-sphere-viewer/core": "^5.7.1", - "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", - "@photo-sphere-viewer/video-plugin": "^5.7.2", + "@photo-sphere-viewer/core": "^5.11.5", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", + "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", - "intl-messageformat": "^10.5.14", + "intl-messageformat": "^10.7.11", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", From f70ee3f3502899e68a797ab2104b7a56911ed81a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 14 Jan 2025 09:14:28 -0500 Subject: [PATCH 036/184] refactor: auth pages (#15328) --- web/eslint.config.mjs | 1 + web/src/app.html | 6 + .../forms/admin-registration-form.svelte | 78 -------- .../forms/change-password-form.svelte | 64 ------- .../lib/components/forms/login-form.svelte | 177 ----------------- .../AuthPageLayout.svelte} | 2 +- web/src/routes/+layout.svelte | 24 +-- .../routes/auth/change-password/+page.svelte | 47 ++++- web/src/routes/auth/login/+page.svelte | 179 +++++++++++++++++- web/src/routes/auth/register/+page.svelte | 76 +++++++- 10 files changed, 293 insertions(+), 361 deletions(-) delete mode 100644 web/src/lib/components/forms/admin-registration-form.svelte delete mode 100644 web/src/lib/components/forms/change-password-form.svelte delete mode 100644 web/src/lib/components/forms/login-form.svelte rename web/src/lib/components/{shared-components/fullscreen-container.svelte => layouts/AuthPageLayout.svelte} (93%) diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index f3cf9d7f10b72..fc5e35ce6db92 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -81,6 +81,7 @@ export default [ 'unicorn/prevent-abbreviations': 'off', 'unicorn/no-nested-ternary': 'off', 'unicorn/consistent-function-scoping': 'off', + 'unicorn/filename-case': 'off', 'unicorn/prefer-top-level-await': 'off', 'unicorn/import-style': 'off', 'svelte/button-has-type': 'error', diff --git a/web/src/app.html b/web/src/app.html index 6fd02dc9f811b..c0ac3cfe6c2e3 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -101,6 +101,12 @@ + +
diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte deleted file mode 100644 index b4ecd56283195..0000000000000 --- a/web/src/lib/components/forms/admin-registration-form.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- - {#if errorMessage} -

{errorMessage}

- {/if} - -
- -
- diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte deleted file mode 100644 index 6f16781d9a037..0000000000000 --- a/web/src/lib/components/forms/change-password-form.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -
-
- - -
- -
- - -
- - {#if errorMessage} -

{errorMessage}

- {/if} - -
- -
-
diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte deleted file mode 100644 index 6c1dcecba3dd5..0000000000000 --- a/web/src/lib/components/forms/login-form.svelte +++ /dev/null @@ -1,177 +0,0 @@ - - -{#if !oauthLoading && $featureFlags.passwordLogin} -
- {#if errorMessage} -

- {errorMessage} -

- {/if} - -
- - -
- -
- - -
- -
- -
-
-{/if} - -{#if $featureFlags.oauth} - {#if $featureFlags.passwordLogin} -
-
- - {$t('or')} - -
- {/if} -
- {#if oauthError} -

{oauthError}

- {/if} - -
-{/if} - -{#if !$featureFlags.passwordLogin && !$featureFlags.oauth} -

{$t('login_has_been_disabled')}

-{/if} diff --git a/web/src/lib/components/shared-components/fullscreen-container.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte similarity index 93% rename from web/src/lib/components/shared-components/fullscreen-container.svelte rename to web/src/lib/components/layouts/AuthPageLayout.svelte index 64ee41a2255cc..c470f809a6bff 100644 --- a/web/src/lib/components/shared-components/fullscreen-container.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -1,6 +1,6 @@ - + {#snippet message()}

{$t('hi_user', { values: { name: $user.name, email: $user.email } })} @@ -31,5 +44,23 @@

{/snippet} - -
+
+
+ + +
+ +
+ + +
+ + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ +
+
+ diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 0ab506f5e3ca6..63346a6abf69f 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -1,9 +1,17 @@ {#if $featureFlags.loaded} - + {#snippet message()}

@@ -22,10 +111,82 @@

{/snippet} - await goto(AppRoute.PHOTOS, { invalidateAll: true })} - onFirstLogin={async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD)} - onOnboarding={async () => await goto(AppRoute.AUTH_ONBOARDING)} - /> -
+ {#if !oauthLoading && $featureFlags.passwordLogin} +
+ {#if errorMessage} +

+ {errorMessage} +

+ {/if} + +
+ + +
+ +
+ + +
+ +
+ +
+
+ {/if} + + {#if $featureFlags.oauth} + {#if $featureFlags.passwordLogin} +
+
+ + {$t('or')} + +
+ {/if} +
+ {#if oauthError} +

{oauthError}

+ {/if} + +
+ {/if} + + {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} +

{$t('login_has_been_disabled')}

+ {/if} + {/if} diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index 2e55ba7435ecd..43e28d5964687 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -1,22 +1,86 @@ - + {#snippet message()}

{$t('admin.registration_description')}

{/snippet} - -
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ +
+
+ From 4279cd6e1e98685aa55e49758e8adc3c97c0ecf2 Mon Sep 17 00:00:00 2001 From: Mattia Natali Date: Tue, 14 Jan 2025 15:24:58 +0100 Subject: [PATCH 037/184] feat(web): Slideshow is enabled everywhere. It no longer needs assetStore. (#15077) Slideshow no longer needs assetStore. It is enabled everywhere Co-authored-by: Alex --- .../asset-viewer/asset-viewer.svelte | 52 ++++++++----------- .../components/photos-page/asset-grid.svelte | 14 ++++- .../gallery-viewer/gallery-viewer.svelte | 51 +++++++++++++++--- .../duplicates-compare-control.svelte | 39 +++++++++++--- web/src/lib/stores/asset-viewing.store.ts | 3 +- .../[[assetId=id]]/+page.svelte | 15 ++++++ 6 files changed, 128 insertions(+), 46 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7a2f97bb655ee..ea5d6e92759cb 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -8,7 +8,6 @@ import { updateNumberOfComments } from '$lib/stores/activity.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { AssetStore } from '$lib/stores/assets.store'; import { isShowDetail } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; @@ -49,8 +48,9 @@ import VideoViewer from './video-wrapper-viewer.svelte'; import ImagePanoramaViewer from './image-panorama-viewer.svelte'; + type HasAsset = boolean; + interface Props { - assetStore?: AssetStore | null; asset: AssetResponseDto; preloadAssets?: AssetResponseDto[]; showNavigation?: boolean; @@ -61,13 +61,13 @@ onAction?: OnAction | undefined; reactions?: ActivityResponseDto[]; onClose: (dto: { asset: AssetResponseDto }) => void; - onNext: () => void; - onPrevious: () => void; + onNext: () => Promise; + onPrevious: () => Promise; + onRandom: () => Promise; copyImage?: () => Promise; } let { - assetStore = null, asset = $bindable(), preloadAssets = $bindable([]), showNavigation = true, @@ -80,6 +80,7 @@ onClose, onNext, onPrevious, + onRandom, copyImage = $bindable(), }: Props = $props(); @@ -271,22 +272,6 @@ }); }; - const navigateAssetRandom = async () => { - if (!assetStore) { - return; - } - - const asset = await assetStore.getRandomAsset(); - if (!asset) { - return; - } - - slideshowHistory.queue(asset); - - setAsset(asset); - $restartSlideshowProgress = true; - }; - const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { if (!order) { if ($slideshowState === SlideshowState.PlaySlideshow) { @@ -296,23 +281,30 @@ } } + e?.stopPropagation(); + + let hasNext = false; + if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom(); + hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + if (!hasNext) { + const asset = await onRandom(); + if (asset) { + slideshowHistory.queue(asset); + hasNext = true; + } + } + } else { + hasNext = order === 'previous' ? await onPrevious() : await onNext(); } - if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) { - const hasNext = - order === 'previous' ? await assetStore.getPreviousAsset(asset) : await assetStore.getNextAsset(asset); + if ($slideshowState === SlideshowState.PlaySlideshow) { if (hasNext) { $restartSlideshowProgress = true; } else { await handleStopSlideshow(); } } - - e?.stopPropagation(); - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - order === 'previous' ? onPrevious() : onNext(); }; // const showEditorHandler = () => { @@ -435,7 +427,7 @@ {person} {stack} showDetailButton={enableDetailPanel} - showSlideshow={!!assetStore} + showSlideshow={true} onZoomImage={zoomToggle} onCopyImage={copyImage} onAction={handleAction} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 55f935c8ddff7..fd98f7e6a3a10 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -527,6 +527,18 @@ return !!nextAsset; }; + const handleRandom = async () => { + const randomAsset = await $assetStore.getRandomAsset(); + + if (randomAsset) { + const preloadAsset = await $assetStore.getNextAsset(randomAsset); + assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []); + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + } + + return randomAsset; + }; + const handleClose = async ({ asset }: { asset: AssetResponseDto }) => { assetViewingStore.showAssetViewer(false); showSkeleton = true; @@ -911,7 +923,6 @@ {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} {/await} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 65c6c20e7b75b..4c3c35aecab44 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -34,6 +34,7 @@ isShowDeleteConfirmation?: boolean; onPrevious?: (() => Promise) | undefined; onNext?: (() => Promise) | undefined; + onRandom?: (() => Promise) | undefined; } let { @@ -47,6 +48,7 @@ isShowDeleteConfirmation = $bindable(false), onPrevious = undefined, onNext = undefined, + onRandom = undefined, }: Props = $props(); let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; @@ -202,35 +204,71 @@ })(), ); - const handleNext = async () => { + const handleNext = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onNext) { asset = await onNext(); } else { - currentViewAssetIndex = Math.min(currentViewAssetIndex + 1, assets.length - 1); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex + 1; + asset = currentViewAssetIndex < assets.length ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; + } + + await navigateToAsset(asset); + return true; + } catch (error) { + handleError(error, $t('errors.cannot_navigate_next_asset')); + return false; + } + }; + + const handleRandom = async (): Promise => { + try { + let asset: AssetResponseDto | undefined; + if (onRandom) { + asset = await onRandom(); + } else { + if (assets.length > 0) { + const randomIndex = Math.floor(Math.random() * assets.length); + asset = assets[randomIndex]; + } + } + + if (!asset) { + return null; } await navigateToAsset(asset); + return asset; } catch (error) { handleError(error, $t('errors.cannot_navigate_next_asset')); + return null; } }; - const handlePrevious = async () => { + const handlePrevious = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onPrevious) { asset = await onPrevious(); } else { - currentViewAssetIndex = Math.max(currentViewAssetIndex - 1, 0); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex - 1; + asset = currentViewAssetIndex >= 0 ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; } await navigateToAsset(asset); + return true; } catch (error) { handleError(error, $t('errors.cannot_navigate_previous_asset')); + return false; } }; @@ -372,6 +410,7 @@ onAction={handleAction} onPrevious={handlePrevious} onNext={handleNext} + onRandom={handleRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index 11a5c67fcfb99..e6b995434901e 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -42,6 +42,34 @@ assetViewingStore.showAssetViewer(false); }); + const onNext = () => { + const index = getAssetIndex($viewingAsset.id) + 1; + if (index >= assets.length) { + return Promise.resolve(false); + } + setAsset(assets[index]); + return Promise.resolve(true); + }; + + const onPrevious = () => { + const index = getAssetIndex($viewingAsset.id) - 1; + if (index < 0) { + return Promise.resolve(false); + } + setAsset(assets[index]); + return Promise.resolve(true); + }; + + const onRandom = () => { + if (assets.length <= 0) { + return Promise.resolve(null); + } + const index = Math.floor(Math.random() * assets.length); + const asset = assets[index]; + setAsset(asset); + return Promise.resolve(asset); + }; + const onSelectAsset = (asset: AssetResponseDto) => { if (selectedAssetIds.has(asset.id)) { selectedAssetIds.delete(asset.id); @@ -153,14 +181,9 @@ 1} - onNext={() => { - const index = getAssetIndex($viewingAsset.id) + 1; - setAsset(assets[index % assets.length]); - }} - onPrevious={() => { - const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; - setAsset(assets[index % assets.length]); - }} + {onNext} + {onPrevious} + {onRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index 2e6e44511d36b..689556b52242a 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -15,9 +15,10 @@ function createAssetViewingStore() { viewState.set(true); }; - const setAssetId = async (id: string) => { + const setAssetId = async (id: string): Promise => { const asset = await getAssetInfo({ id, key: getKey() }); setAsset(asset); + return asset; }; const showAssetViewer = (show: boolean) => { diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 613ae4d66bed7..2239a21cd5699 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -107,14 +107,28 @@ if (viewingAssetCursor < viewingAssets.length - 1) { await setAssetId(viewingAssets[++viewingAssetCursor]); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return true; } + return false; } async function navigatePrevious() { if (viewingAssetCursor > 0) { await setAssetId(viewingAssets[--viewingAssetCursor]); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return true; } + return false; + } + + async function navigateRandom() { + if (viewingAssets.length <= 0) { + return null; + } + const index = Math.floor(Math.random() * viewingAssets.length); + const asset = await setAssetId(viewingAssets[index]); + await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + return asset; } @@ -132,6 +146,7 @@ showNavigation={viewingAssets.length > 1} onNext={navigateNext} onPrevious={navigatePrevious} + onRandom={navigateRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); From 19e2504583595d4c5e6ce847a89e4dacdf762ad2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:19:01 -0500 Subject: [PATCH 038/184] fix(deps): update machine-learning (#15336) --- machine-learning/Dockerfile | 4 ++-- machine-learning/poetry.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 705e4827ff1e0..925e027ff64b9 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:b337e1fd27dbacda505219f713789bf82766694095876769ea10c2d34b4f470b AS builder-cpu +FROM python:3.11-bookworm@sha256:f997d3f71b7dcff3f937703c02861437f2b41a94e1ddbd1b5fa357ee99f5cce4 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:873952659a04188d2a62d5f7e30fd673d2559432a847a8ad5fcaf9cbd085e9ed AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:de909939264a469f834cf89e4dd2ed6aca2ef0844f47ba0bf7f2b4661f5111a5 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 33a4354c3065e..ebfd075c7c7d3 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2498,13 +2498,13 @@ files = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, + {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, + {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, ] [package.dependencies] From 3e11b90851819374342d493d6c575f0c14381463 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:20:12 -0500 Subject: [PATCH 039/184] chore(deps): update node.js to v22.13.0 (#15337) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/Dockerfile | 2 +- server/Dockerfile | 2 +- web/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/Dockerfile b/cli/Dockerfile index 31dd8576e2eaf..da2f17cc39014 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS core +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/Dockerfile b/server/Dockerfile index 85c3ffae1f242..7a54578770bc3 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS web +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/web/Dockerfile b/web/Dockerfile index dfef1d83481d3..0b510004262b5 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 RUN apk add --no-cache tini USER node From 073fccb517afb13335e9428e2f9082d2c9137b48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:33:27 +0000 Subject: [PATCH 040/184] chore(deps): update python:3.11-slim-bookworm docker digest to 6ed5bff (#15346) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 925e027ff64b9..7b0f97c1cf8b7 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:de909939264a469f834cf89e4dd2ed6aca2ef0844f47ba0bf7f2b4661f5111a5 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS prod-cpu FROM prod-cpu AS prod-openvino From b9000d8770109bbd145040e13de07c3f5e16927f Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 14 Jan 2025 14:53:33 -0500 Subject: [PATCH 041/184] feat(web): immich-ui components (#14263) * feat: add immich-ui to auth pages * fix: welcome icon * styling * fix: mobile padding --------- Co-authored-by: Alex Tran --- docker/docker-compose.dev.yml | 1 + docs/docs/developer/setup.md | 11 + web/package-lock.json | 317 +++++++++++------- web/package.json | 1 + web/src/app.css | 24 ++ .../components/layouts/AuthPageLayout.svelte | 39 +-- web/src/routes/+layout.svelte | 10 + web/src/routes/+page.svelte | 11 +- .../routes/auth/change-password/+page.svelte | 50 ++- web/src/routes/auth/login/+page.svelte | 80 ++--- web/src/routes/auth/register/+page.svelte | 47 ++- web/tailwind.config.js | 15 +- web/vite.config.js | 1 + 13 files changed, 345 insertions(+), 262 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index fc1e2602daf1c..4dc41e143e003 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -71,6 +71,7 @@ services: - ../web:/usr/src/app - ../i18n:/usr/src/i18n - ../open-api/:/usr/src/open-api/ + # - ../../ui:/usr/ui - /usr/src/app/node_modules ulimits: nofile: diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 9dbaf157b5d49..f341c3e9cbd3d 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -63,6 +63,17 @@ If you only want to do web development connected to an existing, remote backend, IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev ``` +#### `@immich/ui` + +To see local changes to `@immich/ui` in Immich, do the following: + +1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` +1. Build the `@immich/ui` project via `npm run build` +1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) +1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) +1. Start up the stack via `make dev` +1. After making changes in `@immich/ui`, rebuild it (`npm run build`) + ### Mobile app The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system. diff --git a/web/package-lock.json b/web/package-lock.json index 426df0acd78d6..14d0928731bb7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/ui": "^0.11.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -104,7 +105,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "engines": { "node": ">=10" }, @@ -793,6 +793,31 @@ "npm": ">=9.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@formatjs/ecma402-abstract": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", @@ -1279,11 +1304,34 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@immich/ui": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.11.0.tgz", + "integrity": "sha512-zRQFHCVt6BstNkGuVt27rLUAurOpZ0djfaZYDeqHuc8H97XXXk+hsbXzvADlVa9xAPHetUM3JuusPseJ+Hr23g==", + "license": "GNU Affero General Public License version 3", + "dependencies": { + "@mdi/js": "^7.4.47", + "bits-ui": "^1.0.0-next.46", + "tailwind-merge": "^2.5.4", + "tailwind-variants": "^0.3.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@internationalized/date": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz", + "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1300,7 +1348,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -1312,7 +1359,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -1323,14 +1369,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1347,7 +1391,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1362,7 +1405,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1542,7 +1584,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1555,7 +1596,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1564,7 +1604,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1605,7 +1644,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -2017,6 +2055,15 @@ "vite": "^5.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.2.0.tgz", @@ -2826,7 +2873,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2846,14 +2892,12 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2865,8 +2909,7 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -2967,18 +3010,40 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/bits-ui": { + "version": "1.0.0-next.77", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.77.tgz", + "integrity": "sha512-IV0AyVEvsRkXv4s/fl4iea5E9W2b9EBf98s9mRMKMc1xHxM9MmtM2r6MZMqftHQ/c+gHTIt3A9EKuTlh7uay8w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.4", + "@floating-ui/dom": "^1.6.7", + "@internationalized/date": "^3.5.6", + "esm-env": "^1.1.2", + "runed": "^0.22.0", + "svelte-toolbelt": "^0.7.0" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "svelte": "^5.11.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2993,7 +3058,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3093,7 +3157,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "engines": { "node": ">= 6" } @@ -3164,7 +3227,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3189,7 +3251,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3350,7 +3411,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "engines": { "node": ">= 6" } @@ -3388,7 +3448,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3416,7 +3475,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -3580,14 +3638,12 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/dom-accessibility-api": { "version": "0.5.16", @@ -3621,8 +3677,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { "version": "1.5.74", @@ -3634,8 +3689,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/engine.io-client": { "version": "6.5.4", @@ -4146,9 +4200,9 @@ } }, "node_modules/esm-env": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", - "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "license": "MIT" }, "node_modules/esniff": { @@ -4306,7 +4360,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4323,7 +4376,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4347,7 +4399,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -4374,7 +4425,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4424,7 +4474,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -4469,7 +4518,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -4482,8 +4530,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/geojson-vt": { "version": "3.2.1", @@ -4527,7 +4574,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4548,7 +4594,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4560,7 +4605,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4570,7 +4614,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4666,7 +4709,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4852,6 +4894,12 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -4882,7 +4930,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4909,7 +4956,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -4944,7 +4990,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4953,7 +4998,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -4962,7 +5006,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4974,7 +5017,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5113,7 +5155,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -5128,7 +5169,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -5303,8 +5343,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-character": { "version": "3.0.0", @@ -5546,7 +5585,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -5555,7 +5593,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5623,7 +5660,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -5660,7 +5696,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -5671,7 +5706,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -5734,7 +5768,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5760,7 +5793,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5769,7 +5801,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -5850,8 +5881,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -5910,7 +5940,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5918,14 +5947,12 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -5940,8 +5967,7 @@ "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/pathe": { "version": "1.1.2", @@ -5976,14 +6002,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -5995,7 +6019,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6004,7 +6027,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "engines": { "node": ">= 6" } @@ -6031,7 +6053,6 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.0.tgz", "integrity": "sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6060,7 +6081,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -6077,7 +6097,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -6125,7 +6144,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6194,7 +6212,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6207,8 +6224,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/potpack": { "version": "2.0.0", @@ -6341,7 +6357,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -6372,7 +6387,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -6483,7 +6497,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -6561,7 +6574,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -6587,7 +6599,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6697,7 +6708,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -6716,6 +6726,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/runed": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.22.0.tgz", + "integrity": "sha512-ZWVXWhOr0P5xdNgtviz6D1ivLUDWKLCbeC5SUEJ3zBkqLReVqWHenFxMNFeFaiC5bfxhFxyxzyzB+98uYFtwdA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -6843,7 +6868,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6855,7 +6879,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6870,7 +6893,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -6979,7 +7001,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7078,7 +7099,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7093,7 +7113,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7107,7 +7126,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7120,7 +7138,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7152,11 +7169,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -7199,7 +7224,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7841,6 +7865,41 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, + "node_modules/svelte-toolbelt": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.0.tgz", + "integrity": "sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.20.0", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.20.0.tgz", + "integrity": "sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/svelte/node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -7858,11 +7917,36 @@ "optional": true, "peer": true }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.0.tgz", + "integrity": "sha512-ho2k5kn+LB1fT5XdNS3Clb96zieWxbStE9wNLK7D0AV64kdZMaYzAKo0fWl6fXLPY99ffF9oBJnIj5escEl/8A==", + "license": "MIT", + "dependencies": { + "tailwind-merge": "^2.5.4" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -7900,7 +7984,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -7913,7 +7996,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7949,7 +8031,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8000,7 +8081,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -8009,7 +8089,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -8098,7 +8177,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8163,8 +8241,7 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tslib": { "version": "2.8.1", @@ -8308,8 +8385,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -8581,7 +8657,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8635,7 +8710,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8652,7 +8726,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -8667,7 +8740,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -8678,8 +8750,7 @@ "node_modules/wrap-ansi-cjs/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", diff --git a/web/package.json b/web/package.json index b843f6c13af8d..fc936efdc4007 100644 --- a/web/package.json +++ b/web/package.json @@ -67,6 +67,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/ui": "^0.11.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/app.css b/web/src/app.css index d1af865bcadfa..00cefc5ce60de 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -22,6 +22,30 @@ --immich-dark-success: 56 142 60; --immich-dark-warning: 245 124 0; } + + :root { + /* light */ + --immich-ui-primary: 66 80 175; + --immich-ui-dark: 0 0 0; + --immich-ui-light: 255 255 255; + --immich-ui-success: 34 197 94; + --immich-ui-danger: 180 0 0; + --immich-ui-warning: 255 170 0; + --immich-ui-info: 14 165 233; + --immich-ui-default-border: 209 213 219; + } + + .dark { + /* dark */ + --immich-ui-primary: 172 203 250; + --immich-ui-light: 0 0 0; + --immich-ui-dark: 229 231 235; + /* --immich-success: 56 142 60; */ + --immich-ui-danger: 239 68 68; + --immich-ui-warning: 255 170 0; + --immich-ui-info: 14 165 233; + --immich-ui-default-border: 55 65 81; + } } @font-face { diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index c470f809a6bff..3a61a1671cc2d 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -1,36 +1,25 @@ -
-
-
- -

- {title} -

-
- - {#if showMessage} -
- {@render message?.()} -
- {/if} - - {@render children?.()} -
+
+ + + + + {title} + + + + {@render children?.()} + +
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index fa1351ab20d98..2706ead46e094 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -19,12 +19,22 @@ import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import { onDestroy, onMount, type Snippet } from 'svelte'; import { run } from 'svelte/legacy'; + import { setTranslations } from '@immich/ui'; import '../app.css'; + import { t } from 'svelte-i18n'; interface Props { children?: Snippet; } + $effect(() => { + setTranslations({ + close: $t('close'), + showPassword: $t('show_password'), + hidePassword: $t('hide_password'), + }); + }); + let { children }: Props = $props(); let showNavigationLoadingBar = $state(false); diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 68a5deb0f9157..b3ac52bd7c0df 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,17 +1,16 @@
-
+
- +
-

{$t('welcome_to_immich')}

-
diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index ea340ff600728..6b911184752db 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -1,11 +1,10 @@ - {#snippet message()} -

- {$t('hi_user', { values: { name: $user.name, email: $user.email } })} -
-
- {$t('change_password_description')} -

- {/snippet} +
+ + + {$t('hi_user', { values: { name: $user.name, email: $user.email } })} + {$t('change_password_description')} + + +
-
-
- - -
+ + + + + -
- - -
+ + + {#if errorMessage} + {errorMessage} + {/if} + - {#if errorMessage} -

{errorMessage}

- {/if} - -
- -
+
+ +
+
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 63346a6abf69f..f52face78eb31 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -1,17 +1,14 @@ {#if $featureFlags.loaded} - - {#snippet message()} -

+ + {#if $serverConfig.loginPageMessage} + {@html $serverConfig.loginPageMessage} -

- {/snippet} + + {/if} {#if !oauthLoading && $featureFlags.passwordLogin} -
+ {#if errorMessage} -

- {errorMessage} -

+ {/if} -
- - -
+ + + -
- - -
+ + + -
- -
+ {/if} {#if $featureFlags.oauth} {#if $featureFlags.passwordLogin} -
+

- {$t('or')} + {$t('or').toUpperCase()}
{/if} -
+
{#if oauthError} -

{oauthError}

+ {/if}
{/if} {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} -

{$t('login_has_been_disabled')}

+ {/if} {/if} diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index 43e28d5964687..50551358ea698 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -1,12 +1,11 @@ - {#snippet message()} -

- {$t('admin.registration_description')} -

- {/snippet} +
+ + {$t('admin.registration_description')} + +
-
-
- - -
+ + + + -
- - -
+ + + -
- - -
+ + + -
- - -
+ + + {#if errorMessage} -

{errorMessage}

+ {/if}
- +
diff --git a/web/tailwind.config.js b/web/tailwind.config.js index eb1ea78fae76f..12bfd7c604da4 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -2,7 +2,7 @@ import plugin from 'tailwindcss/plugin'; /** @type {import('tailwindcss').Config} */ export default { - content: ['./src/**/*.{html,js,svelte,ts}'], + content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/@immich/ui/dist/**/*.{svelte,js}'], darkMode: 'class', theme: { extend: { @@ -24,7 +24,20 @@ export default { 'immich-dark-error': 'rgb(var(--immich-dark-error) / )', 'immich-dark-success': 'rgb(var(--immich-dark-success) / )', 'immich-dark-warning': 'rgb(var(--immich-dark-warning) / )', + + primary: 'rgb(var(--immich-ui-primary) / )', + light: 'rgb(var(--immich-ui-light) / )', + dark: 'rgb(var(--immich-ui-dark) / )', + success: 'rgb(var(--immich-ui-success) / )', + danger: 'rgb(var(--immich-ui-danger) / )', + warning: 'rgb(var(--immich-ui-warning) / )', + info: 'rgb(var(--immich-ui-info) / )', + subtle: 'rgb(var(--immich-gray) / )', }, + borderColor: ({ theme }) => ({ + ...theme('colors'), + DEFAULT: 'rgb(var(--immich-ui-default-border) / )', + }), fontFamily: { 'immich-mono': ['Overpass Mono', 'monospace'], }, diff --git a/web/vite.config.js b/web/vite.config.js index 266312e137dbb..5d134beab081b 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -19,6 +19,7 @@ export default defineConfig({ 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js', // eslint-disable-next-line unicorn/prefer-module '@test-data': path.resolve(__dirname, './src/test-data'), + // '@immich/ui': path.resolve(__dirname, '../../ui'), }, }, server: { From 5d2e421800c47d02d97736ad44abe89c89f430f9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 14 Jan 2025 15:01:21 -0500 Subject: [PATCH 042/184] chore: add renovate config for immich-ui (#15349) --- renovate.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/renovate.json b/renovate.json index dd3ca1ad59e3e..2634eaef4d119 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,10 @@ ], "minimumReleaseAge": "5 days", "packageRules": [ + { + "groupName": "@immich/ui", + "matchPackageNames": ["@immich/ui"] + }, { "matchFileNames": [ "cli/**", From c5476a99b1276c3d5395d744f47fbaf6953a892b Mon Sep 17 00:00:00 2001 From: Tempest <110401501+1-tempest@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:06:01 -0600 Subject: [PATCH 043/184] feat: Add additional env variables for Machine Learning (#15326) * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Update config.py * Add additional variables to preload part ML models * Add additional variables to preload part ML models * Apply formatting * minor update * formatting * root validator * minor update * minor update * minor update * change to support explicit models * minor update * minor change * minor change * minor change * minor update * add logs, resolve errors * minor change * add new enviornment variables * minor revisons * remove comments --- docs/docs/install/environment-variables.md | 38 ++++++++++++---------- machine-learning/app/config.py | 36 ++++++++++++++++++-- machine-learning/app/main.py | 23 +++++++++---- machine-learning/app/test_main.py | 24 +++++++++----- 4 files changed, 87 insertions(+), 34 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 1f34b5c6d00a4..b4cb905a0cbc4 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -148,24 +148,26 @@ Redis (Sentinel) URL example JSON before encoding: ## Machine Learning -| Variable | Description | Default | Containers | -| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | -| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | -| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | -| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | -| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | -| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | -| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning | -| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | -| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | -| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | -| `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | -| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | +| Variable | Description | Default | Containers | +| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | +| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | +| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | +| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Name of the textual CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Name of the visual CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Name of the recognition portion of the facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Name of the detection portion of the facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | +| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | +| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | +| `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | +| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py index 92799ac692ec1..fa7dc61d47fde 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -14,9 +14,41 @@ from uvicorn.workers import UvicornWorker +class ClipSettings(BaseModel): + textual: str | None = None + visual: str | None = None + + +class FacialRecognitionSettings(BaseModel): + recognition: str | None = None + detection: str | None = None + + class PreloadModelData(BaseModel): - clip: str | None = None - facial_recognition: str | None = None + clip: ClipSettings = ClipSettings() + facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings() + + clip_model_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None) + facial_recognition_model_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None) + + def update_from_fallbacks(self) -> None: + if self.clip_model_fallback: + self.clip.textual = self.clip_model_fallback + self.clip.visual = self.clip_model_fallback + log.warning( + "Deprecated env variable: MACHINE_LEARNING_PRELOAD__CLIP. " + "Use MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL and " + "MACHINE_LEARNING_PRELOAD__CLIP__VISUAL instead." + ) + + if self.facial_recognition_model_fallback: + self.facial_recognition.recognition = self.facial_recognition_model_fallback + self.facial_recognition.detection = self.facial_recognition_model_fallback + log.warning( + "Deprecated environment variable: MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION. " + "Use MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION and " + "MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION instead." + ) class MaxBatchSize(BaseModel): diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index 684001b875e41..ee8881b6c604c 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -76,18 +76,29 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def preload_models(preload: PreloadModelData) -> None: log.info(f"Preloading models: {preload}") - if preload.clip is not None: - model = await model_cache.get(preload.clip, ModelType.TEXTUAL, ModelTask.SEARCH) + + if preload.clip.textual is not None: + model = await model_cache.get(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH) await load(model) - model = await model_cache.get(preload.clip, ModelType.VISUAL, ModelTask.SEARCH) + if preload.clip.visual is not None: + model = await model_cache.get(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH) await load(model) - if preload.facial_recognition is not None: - model = await model_cache.get(preload.facial_recognition, ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION) + if preload.facial_recognition.detection is not None: + model = await model_cache.get( + preload.facial_recognition.detection, + ModelType.DETECTION, + ModelTask.FACIAL_RECOGNITION, + ) await load(model) - model = await model_cache.get(preload.facial_recognition, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION) + if preload.facial_recognition.recognition is not None: + model = await model_cache.get( + preload.facial_recognition.recognition, + ModelType.RECOGNITION, + ModelTask.FACIAL_RECOGNITION, + ) await load(model) diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index e5cb63997cfb3..5da3baded7ad8 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -700,11 +700,13 @@ async def test_raises_exception_if_unknown_model_name(self) -> None: await model_cache.get("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH) async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai" settings = Settings() assert settings.preload is not None - assert settings.preload.clip == "ViT-B-32__openai" + assert settings.preload.clip.textual == "ViT-B-32__openai" + assert settings.preload.clip.visual == "ViT-B-32__openai" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) @@ -721,11 +723,13 @@ async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_mod async def test_preloads_facial_recognition_models( self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock ) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s" settings = Settings() assert settings.preload is not None - assert settings.preload.facial_recognition == "buffalo_s" + assert settings.preload.facial_recognition.detection == "buffalo_s" + assert settings.preload.facial_recognition.recognition == "buffalo_s" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) @@ -740,13 +744,17 @@ async def test_preloads_facial_recognition_models( ) async def test_preloads_all_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" - os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s" settings = Settings() assert settings.preload is not None - assert settings.preload.clip == "ViT-B-32__openai" - assert settings.preload.facial_recognition == "buffalo_s" + assert settings.preload.clip.visual == "ViT-B-32__openai" + assert settings.preload.clip.textual == "ViT-B-32__openai" + assert settings.preload.facial_recognition.recognition == "buffalo_s" + assert settings.preload.facial_recognition.detection == "buffalo_s" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) From 2903ad815608f82d81107619f9cef67f65d2d885 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:27:16 -0500 Subject: [PATCH 044/184] refactor(server): migrate album-user repo to kysely (#15351) --- server/src/bin/sync-sql.ts | 1 + server/src/interfaces/album-user.interface.ts | 16 ++++--- server/src/queries/album.user.repository.sql | 25 +++++++++++ .../src/repositories/album-user.repository.ts | 42 ++++++++++++------- server/src/services/album.service.spec.ts | 24 +++++------ server/src/services/album.service.ts | 6 +-- 6 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 server/src/queries/album.user.repository.sql diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 2de4fb4127a39..22dae637509b9 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -86,6 +86,7 @@ class SqlGenerator { this.sqlLogger.logQuery(event.query.sql); } else if (event.level === 'error') { this.sqlLogger.logQueryError(event.error as Error, event.query.sql); + this.sqlLogger.logQuery(event.query.sql); } }, }), diff --git a/server/src/interfaces/album-user.interface.ts b/server/src/interfaces/album-user.interface.ts index d5742ad788434..835e835589e20 100644 --- a/server/src/interfaces/album-user.interface.ts +++ b/server/src/interfaces/album-user.interface.ts @@ -1,14 +1,18 @@ -import { AlbumUserEntity } from 'src/entities/album-user.entity'; +import { Insertable, Selectable, Updateable } from 'kysely'; +import { AlbumsSharedUsersUsers } from 'src/db'; export const IAlbumUserRepository = 'IAlbumUserRepository'; export type AlbumPermissionId = { - albumId: string; - userId: string; + albumsId: string; + usersId: string; }; export interface IAlbumUserRepository { - create(albumUser: Partial): Promise; - update({ userId, albumId }: AlbumPermissionId, albumPermission: Partial): Promise; - delete({ userId, albumId }: AlbumPermissionId): Promise; + create(albumUser: Insertable): Promise>; + update( + id: AlbumPermissionId, + albumPermission: Updateable, + ): Promise>; + delete(id: AlbumPermissionId): Promise; } diff --git a/server/src/queries/album.user.repository.sql b/server/src/queries/album.user.repository.sql new file mode 100644 index 0000000000000..d628e4980a3d1 --- /dev/null +++ b/server/src/queries/album.user.repository.sql @@ -0,0 +1,25 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AlbumUserRepository.create +insert into + "albums_shared_users_users" ("usersId", "albumsId") +values + ($1, $2) +returning + * + +-- AlbumUserRepository.update +update "albums_shared_users_users" +set + "role" = $1 +where + "usersId" = $2 + and "albumsId" = $3 +returning + * + +-- AlbumUserRepository.delete +delete from "albums_shared_users_users" +where + "usersId" = $1 + and "albumsId" = $2 diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 9328ea8cfcb01..5895721b63d06 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -1,26 +1,40 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AlbumUserEntity } from 'src/entities/album-user.entity'; +import { Insertable, Kysely, Selectable, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { AlbumsSharedUsersUsers, DB } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AlbumUserRole } from 'src/enum'; import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface'; -import { Repository } from 'typeorm'; @Injectable() export class AlbumUserRepository implements IAlbumUserRepository { - constructor(@InjectRepository(AlbumUserEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} - async create(albumUser: Partial): Promise { - const { userId, albumId } = await this.repository.save(albumUser); - return this.repository.findOneOrFail({ where: { userId, albumId } }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) + create(albumUser: Insertable): Promise> { + return this.db.insertInto('albums_shared_users_users').values(albumUser).returningAll().executeTakeFirstOrThrow(); } - async update({ userId, albumId }: AlbumPermissionId, dto: Partial): Promise { - await this.repository.update({ userId, albumId }, dto); - return this.repository.findOneOrFail({ - where: { userId, albumId }, - }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) + update( + { usersId, albumsId }: AlbumPermissionId, + dto: Updateable, + ): Promise> { + return this.db + .updateTable('albums_shared_users_users') + .set(dto) + .where('usersId', '=', usersId) + .where('albumsId', '=', albumsId) + .returningAll() + .executeTakeFirstOrThrow(); } - async delete({ userId, albumId }: AlbumPermissionId): Promise { - await this.repository.delete({ userId, albumId }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) + async delete({ usersId, albumsId }: AlbumPermissionId): Promise { + await this.db + .deleteFrom('albums_shared_users_users') + .where('usersId', '=', usersId) + .where('albumsId', '=', albumsId) + .execute(); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index bb1aac8e6e48b..ca6b56e08538b 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -323,18 +323,16 @@ describe(AlbumService.name, () => { albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(userStub.user2); albumUserMock.create.mockResolvedValue({ - userId: userStub.user2.id, - user: userStub.user2, - albumId: albumStub.sharedWithAdmin.id, - album: albumStub.sharedWithAdmin, + usersId: userStub.user2.id, + albumsId: albumStub.sharedWithAdmin.id, role: AlbumUserRole.EDITOR, }); await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: authStub.user2.user.id }], }); expect(albumUserMock.create).toHaveBeenCalledWith({ - userId: authStub.user2.user.id, - albumId: albumStub.sharedWithAdmin.id, + usersId: authStub.user2.user.id, + albumsId: albumStub.sharedWithAdmin.id, }); expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.sharedWithAdmin.id, @@ -361,8 +359,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: userStub.user1.id, + albumsId: albumStub.sharedWithUser.id, + usersId: userStub.user1.id, }); expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false }); }); @@ -388,8 +386,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, + albumsId: albumStub.sharedWithUser.id, + usersId: authStub.user1.user.id, }); }); @@ -400,8 +398,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, + albumsId: albumStub.sharedWithUser.id, + usersId: authStub.user1.user.id, }); }); @@ -433,7 +431,7 @@ describe(AlbumService.name, () => { role: AlbumUserRole.EDITOR, }); expect(albumUserMock.update).toHaveBeenCalledWith( - { albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id }, + { albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id }, { role: AlbumUserRole.EDITOR }, ); }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index e57e6b168ce17..f5685f84ebdf8 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -229,7 +229,7 @@ export class AlbumService extends BaseService { throw new BadRequestException('User not found'); } - await this.albumUserRepository.create({ userId, albumId: id, role }); + await this.albumUserRepository.create({ usersId: userId, albumsId: id, role }); await this.eventRepository.emit('album.invite', { id, userId }); } @@ -257,12 +257,12 @@ export class AlbumService extends BaseService { await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } - await this.albumUserRepository.delete({ albumId: id, userId }); + await this.albumUserRepository.delete({ albumsId: id, usersId: userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); - await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); + await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); } private async findOrFail(id: string, options: AlbumInfoOptions) { From 43b3181f45b3b3366400c546743674a0d59e04b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:58:02 -0500 Subject: [PATCH 045/184] chore(deps): update base-image to v20250114 (major) (#15347) chore(deps): update base-image to v20250114 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 7a54578770bc3..84902df3ca3ec 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20250107@sha256:d00ab37e1c1ed87b799d6509fbc825a721ca0723c59c67955217826882017d38 AS dev +FROM ghcr.io/immich-app/base-server-dev:20250114@sha256:fce0404484bde5afc38a4399c6b25895eb079a666d269f199c93dfbfdd5b26b6 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -42,7 +42,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20250107@sha256:78e92f113103271d43a3b050370b21b31c3c14792d3d23b18b542581a440c72b +FROM ghcr.io/immich-app/base-server-prod:20250114@sha256:94ec8a36cdf11691810c4aeccee1b49b00348e17f6b6781d87dd48a74e6c6787 WORKDIR /usr/src/app ENV NODE_ENV=production \ From 93e25452758c2480d0aa3e053ae205b00af4e0e5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Jan 2025 11:34:11 -0500 Subject: [PATCH 046/184] refactor: migrate memory to kysely (#15314) --- server/src/interfaces/memory.interface.ts | 11 +- server/src/queries/memory.repository.sql | 83 +++++++++++-- server/src/repositories/memory.repository.ts | 119 ++++++++++--------- server/src/services/memory.service.spec.ts | 23 ++-- server/src/services/memory.service.ts | 28 ++--- 5 files changed, 176 insertions(+), 88 deletions(-) diff --git a/server/src/interfaces/memory.interface.ts b/server/src/interfaces/memory.interface.ts index 308943d97e84e..b1dbcbef8562a 100644 --- a/server/src/interfaces/memory.interface.ts +++ b/server/src/interfaces/memory.interface.ts @@ -1,4 +1,6 @@ -import { MemoryEntity } from 'src/entities/memory.entity'; +import { Insertable, Updateable } from 'kysely'; +import { Memories } from 'src/db'; +import { MemoryEntity, OnThisDayData } from 'src/entities/memory.entity'; import { IBulkAsset } from 'src/utils/asset.util'; export const IMemoryRepository = 'IMemoryRepository'; @@ -6,7 +8,10 @@ export const IMemoryRepository = 'IMemoryRepository'; export interface IMemoryRepository extends IBulkAsset { search(ownerId: string): Promise; get(id: string): Promise; - create(memory: Partial): Promise; - update(memory: Partial): Promise; + create( + memory: Omit, 'data'> & { data: OnThisDayData }, + assetIds: Set, + ): Promise; + update(id: string, memory: Updateable): Promise; delete(id: string): Promise; } diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index e3945ca02828f..396da3f56e733 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -1,10 +1,79 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MemoryRepository.search +select + * +from + "memories" +where + "ownerId" = $1 +order by + "memoryAt" desc + +-- MemoryRepository.get +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.update +update "memories" +set + "ownerId" = $1, + "isSaved" = $2 +where + "id" = $3 +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.delete +delete from "memories" +where + "id" = $1 + -- MemoryRepository.getAssetIds -SELECT - "memories_assets"."assetsId" AS "assetId" -FROM - "memories_assets_assets" "memories_assets" -WHERE - "memories_assets"."memoriesId" = $1 - AND "memories_assets"."assetsId" IN ($2) +select + "assetsId" +from + "memories_assets_assets" +where + "memoriesId" = $1 + and "assetsId" in ($2) diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 47dc705093c23..7e59b92e68b55 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -1,49 +1,55 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Memories } from 'src/db'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { MemoryEntity } from 'src/entities/memory.entity'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; -import { DataSource, In, Repository } from 'typeorm'; @Injectable() export class MemoryRepository implements IMemoryRepository { - constructor( - @InjectRepository(MemoryEntity) private repository: Repository, - @InjectDataSource() private dataSource: DataSource, - ) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [DummyValue.UUID] }) search(ownerId: string): Promise { - return this.repository.find({ - where: { - ownerId, - }, - order: { - memoryAt: 'DESC', - }, - }); + return this.db + .selectFrom('memories') + .selectAll() + .where('ownerId', '=', ownerId) + .orderBy('memoryAt', 'desc') + .execute() as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) get(id: string): Promise { - return this.repository.findOne({ - where: { - id, - }, - relations: { - assets: true, - }, - }); + return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise; } - create(memory: Partial): Promise { - return this.save(memory); + async create(memory: Insertable, assetIds: Set): Promise { + const id = await this.db.transaction().execute(async (tx) => { + const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow(); + + if (assetIds.size > 0) { + const values = [...assetIds].map((assetId) => ({ memoriesId: id, assetsId: assetId })); + await tx.insertInto('memories_assets_assets').values(values).execute(); + } + + return id; + }); + + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } - update(memory: Partial): Promise { - return this.save(memory); + @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) + async update(id: string, memory: Updateable): Promise { + await this.db.updateTable('memories').set(memory).where('id', '=', id).execute(); + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('memories').where('id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @@ -53,46 +59,49 @@ export class MemoryRepository implements IMemoryRepository { return new Set(); } - const results = await this.dataSource - .createQueryBuilder() - .select('memories_assets.assetsId', 'assetId') - .from('memories_assets_assets', 'memories_assets') - .where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) - .andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) - .getRawMany<{ assetId: string }>(); + const results = await this.db + .selectFrom('memories_assets_assets') + .select(['assetsId']) + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) + .execute(); - return new Set(results.map(({ assetId }) => assetId)); + return new Set(results.map(({ assetsId }) => assetsId)); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async addAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .insert() - .into('memories_assets_assets', ['memoriesId', 'assetsId']) + await this.db + .insertInto('memories_assets_assets') .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) .execute(); } @Chunked({ paramIndex: 1 }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async removeAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .delete() - .from('memories_assets_assets') - .where({ - memoriesId: id, - assetsId: In(assetIds), - }) + await this.db + .deleteFrom('memories_assets_assets') + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) .execute(); } - private async save(memory: Partial): Promise { - const { id } = await this.repository.save(memory); - return this.repository.findOneOrFail({ - where: { id }, - relations: { - assets: true, - }, - }); + private getByIdBuilder(id: string) { + return this.db + .selectFrom('memories') + .selectAll('memories') + .select((eb) => + jsonArrayFrom( + eb + .selectFrom('assets') + .selectAll('assets') + .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') + .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') + .where('assets.deletedAt', 'is', null), + ).as('assets'), + ) + .where('id', '=', id) + .where('deletedAt', 'is', null); } } diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index b5dd4c2553f4a..9c5336eb6e809 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -69,7 +69,17 @@ describe(MemoryService.name, () => { memoryAt: new Date(2024), }), ).resolves.toMatchObject({ assets: [] }); - expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); + expect(memoryMock.create).toHaveBeenCalledWith( + { + ownerId: 'admin_id', + memoryAt: expect.any(Date), + type: MemoryType.ON_THIS_DAY, + isSaved: undefined, + sendAt: undefined, + data: { year: 2024 }, + }, + new Set(), + ); }); it('should create a memory', async () => { @@ -80,14 +90,14 @@ describe(MemoryService.name, () => { type: MemoryType.ON_THIS_DAY, data: { year: 2024 }, assetIds: ['asset1'], - memoryAt: new Date(2024), + memoryAt: new Date(2024, 0, 1), }), ).resolves.toBeDefined(); expect(memoryMock.create).toHaveBeenCalledWith( expect.objectContaining({ ownerId: userStub.admin.id, - assets: [{ id: 'asset1' }], }), + new Set(['asset1']), ); }); @@ -115,12 +125,7 @@ describe(MemoryService.name, () => { accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); memoryMock.update.mockResolvedValue(memoryStub.memory1); await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); - expect(memoryMock.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'memory1', - isSaved: true, - }), - ); + expect(memoryMock.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true })); }); }); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 816b0fddeb0fb..926571e43c36c 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @@ -29,15 +28,17 @@ export class MemoryService extends BaseService { permission: Permission.ASSET_SHARE, ids: assetIds, }); - const memory = await this.memoryRepository.create({ - ownerId: auth.user.id, - type: dto.type, - data: dto.data, - isSaved: dto.isSaved, - memoryAt: dto.memoryAt, - seenAt: dto.seenAt, - assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), - }); + const memory = await this.memoryRepository.create( + { + ownerId: auth.user.id, + type: dto.type, + data: dto.data, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + }, + allowedAssetIds, + ); return mapMemory(memory); } @@ -45,8 +46,7 @@ export class MemoryService extends BaseService { async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const memory = await this.memoryRepository.update({ - id, + const memory = await this.memoryRepository.update(id, { isSaved: dto.isSaved, memoryAt: dto.memoryAt, seenAt: dto.seenAt, @@ -68,7 +68,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { updatedAt: new Date() }); } return results; @@ -86,7 +86,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { id, updatedAt: new Date() }); } return results; From 7d087371b5a15e6052e694385b3c68884f439088 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jan 2025 13:55:29 -0600 Subject: [PATCH 047/184] chore: sql sync (#15370) * chore: sql sync * chore: sql sync --- server/src/queries/memory.repository.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index 396da3f56e733..3144f314dde7b 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -77,3 +77,9 @@ from where "memoriesId" = $1 and "assetsId" in ($2) + +-- MemoryRepository.addAssetIds +insert into + "memories_assets_assets" ("memoriesId", "assetsId") +values + ($1, $2) From 2d2966caa02342c67069378cbf27a88d5dbb28d5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Jan 2025 15:03:20 -0500 Subject: [PATCH 048/184] chore: use port 2286 for the auth server (#15369) --- e2e/src/api/specs/oauth.e2e-spec.ts | 4 ++-- e2e/src/setup/auth-server.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 42989a118f7fb..9cd5f0252a642 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -13,8 +13,8 @@ import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; const authServer = { - internal: 'http://auth-server:3000', - external: 'http://127.0.0.1:3000', + internal: 'http://auth-server:2286', + external: 'http://127.0.0.1:2286', }; const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect'; diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts index cde50813ddede..575e97d291af9 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e/src/setup/auth-server.ts @@ -51,7 +51,7 @@ const setup = async () => { const { privateKey, publicKey } = await generateKeyPair('RS256'); const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect']; - const port = 3000; + const port = 2286; const host = '0.0.0.0'; const oidc = new Provider(`http://${host}:${port}`, { renderError: async (ctx, out, error) => { From a60da1ccab9d40c874124b9547df6f1dfc53df50 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Jan 2025 15:09:19 -0500 Subject: [PATCH 049/184] refactor: migrate create user form to immich ui (#15350) * refactor: migrate create user form to immich ui * minor styling tweak * remove unintentional commit * revert formating diff --------- Co-authored-by: Alex Tran --- .../components/forms/create-user-form.svelte | 197 ++++++++---------- .../components/layouts/AuthPageLayout.svelte | 2 +- .../search-bar/search-filter-modal.svelte | 6 +- .../routes/admin/user-management/+page.svelte | 52 +++-- .../routes/auth/change-password/+page.svelte | 4 +- web/src/routes/auth/login/+page.svelte | 2 +- web/tailwind.config.js | 6 +- 7 files changed, 121 insertions(+), 148 deletions(-) diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 7aa1c76ed3216..8bd955734a4a9 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -5,10 +5,8 @@ import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createUserAdmin } from '@immich/sdk'; + import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui'; import { t } from 'svelte-i18n'; - import Button from '../elements/buttons/button.svelte'; - import Slider from '../elements/slider.svelte'; - import PasswordField from '../shared-components/password-field.svelte'; interface Props { onClose: () => void; @@ -17,137 +15,114 @@ oauthEnabled?: boolean; } - let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props(); + let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props(); let error = $state(''); - let success = $state(''); + let success = $state(false); let email = $state(''); let password = $state(''); - let confirmPassword = $state(''); + let passwordConfirm = $state(''); let name = $state(''); let shouldChangePassword = $state(true); let notify = $state(true); - let canCreateUser = $state(false); - let quotaSize: number | undefined = $state(); + let quotaSize: string | undefined = $state(); let isCreatingUser = $state(false); - let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null); + let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null); let quotaSizeWarning = $derived( quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw, ); - $effect(() => { - if (password !== confirmPassword && confirmPassword.length > 0) { - error = $t('password_does_not_match'); - canCreateUser = false; - } else { - error = ''; - canCreateUser = true; - } - }); - - async function registerUser() { - if (canCreateUser && !isCreatingUser) { - isCreatingUser = true; - error = ''; - - try { - await createUserAdmin({ - userAdminCreateDto: { - email, - password, - shouldChangePassword, - name, - quotaSizeInBytes, - notify, - }, - }); - - success = $t('new_user_created'); - - onSubmit(); - - return; - } catch (error) { - handleError(error, $t('errors.unable_to_create_user')); - } finally { - isCreatingUser = false; - } - } - } + const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0); + const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : ''); + const valid = $derived(!passwordMismatch && !isCreatingUser); - const onsubmit = async (event: Event) => { + const onSubmit = async (event: Event) => { event.preventDefault(); - await registerUser(); - }; - - -
-
- - -
- - {#if $featureFlags.email} -
- - -
- {/if} + if (!valid) { + return; + } -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
+ isCreatingUser = true; + error = ''; + + try { + await createUserAdmin({ + userAdminCreateDto: { + email, + password, + shouldChangePassword, + name, + quotaSizeInBytes, + notify, + }, + }); + + success = true; + + onDone(); + + return; + } catch (error) { + handleError(error, $t('errors.unable_to_create_user')); + } finally { + isCreatingUser = false; + } + }; + + + {#if error} -

{error}

+ {/if} {#if success} -

{success}

+

{$t('new_user_created')}

{/if} - - {#snippet stickyBottom()} - - - {/snippet} -
+ + + + + + {#if $featureFlags.email} + + + + {/if} + + + + + + + + {passwordMismatchMessage} + + + + + + + + + + + + + {#if quotaSizeWarning} + {$t('errors.quota_higher_than_disk_size')} + {/if} + + + + {#snippet stickyBottom()} + + + {/snippet} +
+ diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 3a61a1671cc2d..78ff67cfcba5c 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -11,7 +11,7 @@
- + diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte index de34092658a14..c367d001f2eea 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte @@ -17,7 +17,7 @@ - - - - diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index b146f347dc836..25d279b0f401a 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -11,14 +11,12 @@ - -
- - - - - - {#if customDateRange} -
-
- - -
-
- - -
-
- { - customDateRange = false; - settings.dateAfter = ''; - settings.dateBefore = ''; - }} - > - {$t('remove_custom_date_range')} - + + + + + + + + + + + + + + + + + + + + {#if customDateRange} +
+
+ + +
+
+ + +
+
+ +
-
- {:else} -
- -
- { - customDateRange = true; - settings.relativeDate = ''; - }} - > - {$t('use_custom_date_range')} - + {:else} +
+ +
+ +
-
- {/if} - + {/if} + - {#snippet stickyBottom()} - - - {/snippet} - + {#snippet stickyBottom()} + + + {/snippet} + + diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 443e8f06b1d47..77890529b7463 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -1,21 +1,20 @@ -
- +
+ {$t('hi_user', { values: { name: $user.name, email: $user.email } })} {$t('change_password_description')} -
- - - - - - - - - - {errorMessage} - - -
- -
-
+ + + + + + + + {errorMessage} + + +
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index bc062292fcd46..e60cd5f145324 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -6,7 +6,7 @@ import { oauth } from '$lib/utils'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; import { login } from '@immich/sdk'; - import { Alert, Button, Field, Input, PasswordInput } from '@immich/ui'; + import { Alert, Button, Field, Input, PasswordInput, Stack } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -101,43 +101,43 @@ {#if $featureFlags.loaded} - {#if $serverConfig.loginPageMessage} - - - {@html $serverConfig.loginPageMessage} - - {/if} - - {#if !oauthLoading && $featureFlags.passwordLogin} -
- {#if errorMessage} - - {/if} + + {#if $serverConfig.loginPageMessage} + + + {@html $serverConfig.loginPageMessage} + + {/if} + + {#if !oauthLoading && $featureFlags.passwordLogin} + + {#if errorMessage} + + {/if} + + + + + + + + - - - - - - - - - - - {/if} - - {#if $featureFlags.oauth} - {#if $featureFlags.passwordLogin} -
-
- - {$t('or').toUpperCase()} - -
+ + {/if} -
+ + {#if $featureFlags.oauth} + {#if $featureFlags.passwordLogin} +
+
+ + {$t('or').toUpperCase()} + +
+ {/if} {#if oauthError} {/if} @@ -152,11 +152,11 @@ > {$serverConfig.oauthButtonText} -
- {/if} + {/if} - {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} - - {/if} + {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} + + {/if} +
{/if} diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index 50551358ea698..f3bc494d951ce 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -47,13 +47,11 @@ -
- +
+ {$t('admin.registration_description')} -
- @@ -74,8 +72,6 @@ {/if} -
- -
+
From 995314446b64b2e140ff13b4c517bb10cbd9eb55 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jan 2025 16:23:23 -0600 Subject: [PATCH 121/184] feat(web): neon light behinds login form (#15570) --- .../components/layouts/AuthPageLayout.svelte | 7 ++++- web/static/immich-logo-no-bg.svg | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 web/static/immich-logo-no-bg.svg diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 24e28078c1c78..f4532902c2590 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -10,7 +10,12 @@ let { title, children }: Props = $props(); -
+
+
+ Immich logo +
+
+ diff --git a/web/static/immich-logo-no-bg.svg b/web/static/immich-logo-no-bg.svg new file mode 100644 index 0000000000000..376fa6f3e837e --- /dev/null +++ b/web/static/immich-logo-no-bg.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + From 1869b1b41a2a930f002d50057e26e5dbb5af5573 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 23 Jan 2025 18:10:17 -0500 Subject: [PATCH 122/184] refactor: repositories (#15561) * refactor: version history repository * refactor: oauth repository * refactor: trash repository * refactor: telemetry repository * refactor: metadata repository * refactor: cron repository * refactor: map repository * refactor: server-info repository * refactor: album user repository * refactor: notification repository --- server/src/app.module.ts | 5 +- .../controllers/notification.controller.ts | 2 +- server/src/emails/album-invite.email.tsx | 2 +- server/src/emails/album-update.email.tsx | 2 +- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 2 +- server/src/interfaces/album-user.interface.ts | 18 --- server/src/interfaces/cron.interface.ts | 20 ---- server/src/interfaces/job.interface.ts | 2 +- server/src/interfaces/map.interface.ts | 31 ------ server/src/interfaces/metadata.interface.ts | 71 ------------ .../src/interfaces/notification.interface.ts | 101 ----------------- server/src/interfaces/oauth.interface.ts | 22 ---- .../src/interfaces/server-info.interface.ts | 24 ---- server/src/interfaces/telemetry.interface.ts | 23 ---- server/src/interfaces/trash.interface.ts | 8 -- .../interfaces/version-history.interface.ts | 9 -- .../src/repositories/album-user.repository.ts | 13 ++- server/src/repositories/asset.repository.ts | 2 +- server/src/repositories/cron.repository.ts | 17 ++- server/src/repositories/index.ts | 30 ++--- server/src/repositories/map.repository.ts | 33 ++++-- .../src/repositories/metadata.repository.ts | 67 ++++++++++- .../notification.repository.spec.ts | 3 +- .../repositories/notification.repository.ts | 104 ++++++++++++++++-- server/src/repositories/oauth.repository.ts | 17 ++- .../repositories/server-info.repository.ts | 21 +++- .../src/repositories/telemetry.repository.ts | 7 +- server/src/repositories/trash.repository.ts | 3 +- .../version-history.repository.ts | 10 +- server/src/services/album.service.spec.ts | 2 +- server/src/services/auth.service.spec.ts | 3 +- server/src/services/auth.service.ts | 2 +- server/src/services/backup.service.spec.ts | 3 +- server/src/services/base.service.ts | 40 +++---- server/src/services/job.service.spec.ts | 4 +- server/src/services/library.service.spec.ts | 3 +- server/src/services/map.service.spec.ts | 2 +- server/src/services/metadata.service.spec.ts | 5 +- server/src/services/metadata.service.ts | 4 +- .../src/services/notification.service.spec.ts | 3 +- server/src/services/notification.service.ts | 2 +- server/src/services/trash.service.spec.ts | 2 +- server/src/services/version.service.spec.ts | 4 +- server/src/types.ts | 21 ++++ server/test/fixtures/metadata.stub.ts | 2 +- .../album-user.repository.mock.ts | 2 +- .../test/repositories/cron.repository.mock.ts | 2 +- .../test/repositories/map.repository.mock.ts | 2 +- .../repositories/metadata.repository.mock.ts | 2 +- .../notification.repository.mock.ts | 2 +- .../repositories/oauth.repository.mock.ts | 2 +- .../server-info.repository.mock.ts | 2 +- .../repositories/telemetry.repository.mock.ts | 8 +- .../repositories/trash.repository.mock.ts | 2 +- .../version-history.repository.mock.ts | 2 +- server/test/utils.ts | 42 +++++-- 57 files changed, 372 insertions(+), 469 deletions(-) delete mode 100644 server/src/interfaces/album-user.interface.ts delete mode 100644 server/src/interfaces/cron.interface.ts delete mode 100644 server/src/interfaces/map.interface.ts delete mode 100644 server/src/interfaces/metadata.interface.ts delete mode 100644 server/src/interfaces/notification.interface.ts delete mode 100644 server/src/interfaces/oauth.interface.ts delete mode 100644 server/src/interfaces/server-info.interface.ts delete mode 100644 server/src/interfaces/telemetry.interface.ts delete mode 100644 server/src/interfaces/trash.interface.ts delete mode 100644 server/src/interfaces/version-history.interface.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index d088af6188819..cd1997220607c 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -13,7 +13,6 @@ import { entities } from 'src/entities'; import { ImmichWorker } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository } from 'src/interfaces/job.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; @@ -22,7 +21,7 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { providers, repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { teardownTelemetry } from 'src/repositories/telemetry.repository'; +import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; @@ -67,7 +66,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { logger: LoggingRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository, + private telemetryRepository: TelemetryRepository, ) { logger.setAppName(this.worker); } diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index 27034fd63a873..39946a9fc9d71 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -3,8 +3,8 @@ import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; -import { EmailTemplate } from 'src/interfaces/notification.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { EmailTemplate } from 'src/repositories/notification.repository'; import { NotificationService } from 'src/services/notification.service'; @ApiTags('Notifications') diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 0b3819b332b5d..4bd7abc305719 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface'; +import { AlbumInviteEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumInviteEmail = ({ diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 9dcd858e93e03..2311e896e1e4a 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface'; +import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumUpdateEmail = ({ diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index 8ba19436c650d..ac9bdbe0eab83 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,7 +1,7 @@ import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { TestEmailProps } from 'src/interfaces/notification.interface'; +import { TestEmailProps } from 'src/repositories/notification.repository'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index ced0b77698836..11a66027113ef 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { WelcomeEmailProps } from 'src/interfaces/notification.interface'; +import { WelcomeEmailProps } from 'src/repositories/notification.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { diff --git a/server/src/interfaces/album-user.interface.ts b/server/src/interfaces/album-user.interface.ts deleted file mode 100644 index 835e835589e20..0000000000000 --- a/server/src/interfaces/album-user.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Insertable, Selectable, Updateable } from 'kysely'; -import { AlbumsSharedUsersUsers } from 'src/db'; - -export const IAlbumUserRepository = 'IAlbumUserRepository'; - -export type AlbumPermissionId = { - albumsId: string; - usersId: string; -}; - -export interface IAlbumUserRepository { - create(albumUser: Insertable): Promise>; - update( - id: AlbumPermissionId, - albumPermission: Updateable, - ): Promise>; - delete(id: AlbumPermissionId): Promise; -} diff --git a/server/src/interfaces/cron.interface.ts b/server/src/interfaces/cron.interface.ts deleted file mode 100644 index ceb554864a172..0000000000000 --- a/server/src/interfaces/cron.interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const ICronRepository = 'ICronRepository'; - -type CronBase = { - name: string; - start?: boolean; -}; - -export type CronCreate = CronBase & { - expression: string; - onTick: () => void; -}; - -export type CronUpdate = CronBase & { - expression?: string; -}; - -export interface ICronRepository { - create(cron: CronCreate): void; - update(cron: CronUpdate): void; -} diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 7976f813022ff..1f2b92074ac19 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -1,5 +1,5 @@ import { ClassConstructor } from 'class-transformer'; -import { EmailImageAttachment } from 'src/interfaces/notification.interface'; +import { EmailImageAttachment } from 'src/repositories/notification.repository'; export enum QueueName { THUMBNAIL_GENERATION = 'thumbnailGeneration', diff --git a/server/src/interfaces/map.interface.ts b/server/src/interfaces/map.interface.ts deleted file mode 100644 index 0a04840a968a5..0000000000000 --- a/server/src/interfaces/map.interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const IMapRepository = 'IMapRepository'; - -export interface MapMarkerSearchOptions { - isArchived?: boolean; - isFavorite?: boolean; - fileCreatedBefore?: Date; - fileCreatedAfter?: Date; -} - -export interface GeoPoint { - latitude: number; - longitude: number; -} - -export interface ReverseGeocodeResult { - country: string | null; - state: string | null; - city: string | null; -} - -export interface MapMarker extends ReverseGeocodeResult { - id: string; - lat: number; - lon: number; -} - -export interface IMapRepository { - init(): Promise; - reverseGeocode(point: GeoPoint): Promise; - getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise; -} diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts deleted file mode 100644 index 574420e27a1c8..0000000000000 --- a/server/src/interfaces/metadata.interface.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { BinaryField, Tags } from 'exiftool-vendored'; - -export const IMetadataRepository = 'IMetadataRepository'; - -export interface ExifDuration { - Value: number; - Scale?: number; -} - -type StringOrNumber = string | number; - -type TagsWithWrongTypes = - | 'FocalLength' - | 'Duration' - | 'Description' - | 'ImageDescription' - | 'RegionInfo' - | 'TagsList' - | 'Keywords' - | 'HierarchicalSubject' - | 'ISO'; -export interface ImmichTags extends Omit { - ContentIdentifier?: string; - MotionPhoto?: number; - MotionPhotoVersion?: number; - MotionPhotoPresentationTimestampUs?: number; - MediaGroupUUID?: string; - ImagePixelDepth?: string; - FocalLength?: number; - Duration?: number | string | ExifDuration; - EmbeddedVideoType?: string; - EmbeddedVideoFile?: BinaryField; - MotionPhotoVideo?: BinaryField; - TagsList?: StringOrNumber[]; - HierarchicalSubject?: StringOrNumber[]; - Keywords?: StringOrNumber | StringOrNumber[]; - ISO?: number | number[]; - - // Type is wrong, can also be number. - Description?: StringOrNumber; - ImageDescription?: StringOrNumber; - - // Extended properties for image regions, such as faces - RegionInfo?: { - AppliedToDimensions: { - W: number; - H: number; - Unit: string; - }; - RegionList: { - Area: { - // (X,Y) // center of the rectangle - X: number; - Y: number; - W: number; - H: number; - Unit: string; - }; - Rotation?: number; - Type?: string; - Name?: string; - }[]; - }; -} - -export interface IMetadataRepository { - teardown(): Promise; - readTags(path: string): Promise; - writeTags(path: string, tags: Partial): Promise; - extractBinaryTag(tagName: string, path: string): Promise; -} diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts deleted file mode 100644 index b20b3c50aee8f..0000000000000 --- a/server/src/interfaces/notification.interface.ts +++ /dev/null @@ -1,101 +0,0 @@ -export const INotificationRepository = 'INotificationRepository'; - -export type EmailImageAttachment = { - filename: string; - path: string; - cid: string; -}; - -export type SendEmailOptions = { - from: string; - to: string; - replyTo?: string; - subject: string; - html: string; - text: string; - imageAttachments?: EmailImageAttachment[]; - smtp: SmtpOptions; -}; - -export type SmtpOptions = { - host: string; - port?: number; - username?: string; - password?: string; - ignoreCert?: boolean; -}; - -export enum EmailTemplate { - TEST_EMAIL = 'test', - - // AUTH - WELCOME = 'welcome', - RESET_PASSWORD = 'reset-password', - - // ALBUM - ALBUM_INVITE = 'album-invite', - ALBUM_UPDATE = 'album-update', -} - -interface BaseEmailProps { - baseUrl: string; - customTemplate?: string; -} - -export interface TestEmailProps extends BaseEmailProps { - displayName: string; -} - -export interface WelcomeEmailProps extends BaseEmailProps { - displayName: string; - username: string; - password?: string; -} - -export interface AlbumInviteEmailProps extends BaseEmailProps { - albumName: string; - albumId: string; - senderName: string; - recipientName: string; - cid?: string; -} - -export interface AlbumUpdateEmailProps extends BaseEmailProps { - albumName: string; - albumId: string; - recipientName: string; - cid?: string; -} - -export type EmailRenderRequest = - | { - template: EmailTemplate.TEST_EMAIL; - data: TestEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.WELCOME; - data: WelcomeEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.ALBUM_INVITE; - data: AlbumInviteEmailProps; - customTemplate: string; - } - | { - template: EmailTemplate.ALBUM_UPDATE; - data: AlbumUpdateEmailProps; - customTemplate: string; - }; - -export type SendEmailResponse = { - messageId: string; - response: any; -}; - -export interface INotificationRepository { - renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>; - sendEmail(options: SendEmailOptions): Promise; - verifySmtp(options: SmtpOptions): Promise; -} diff --git a/server/src/interfaces/oauth.interface.ts b/server/src/interfaces/oauth.interface.ts deleted file mode 100644 index 5e629726a0a76..0000000000000 --- a/server/src/interfaces/oauth.interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UserinfoResponse } from 'openid-client'; - -export const IOAuthRepository = 'IOAuthRepository'; - -export type OAuthConfig = { - clientId: string; - clientSecret: string; - issuerUrl: string; - mobileOverrideEnabled: boolean; - mobileRedirectUri: string; - profileSigningAlgorithm: string; - scope: string; - signingAlgorithm: string; -}; -export type OAuthProfile = UserinfoResponse; - -export interface IOAuthRepository { - init(): void; - authorize(config: OAuthConfig, redirectUrl: string): Promise; - getLogoutEndpoint(config: OAuthConfig): Promise; - getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise; -} diff --git a/server/src/interfaces/server-info.interface.ts b/server/src/interfaces/server-info.interface.ts deleted file mode 100644 index 6dc857ddea6aa..0000000000000 --- a/server/src/interfaces/server-info.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface GitHubRelease { - id: number; - url: string; - tag_name: string; - name: string; - created_at: string; - published_at: string; - body: string; -} - -export interface ServerBuildVersions { - nodejs: string; - ffmpeg: string; - libvips: string; - exiftool: string; - imagemagick: string; -} - -export const IServerInfoRepository = 'IServerInfoRepository'; - -export interface IServerInfoRepository { - getGitHubRelease(): Promise; - getBuildVersions(): Promise; -} diff --git a/server/src/interfaces/telemetry.interface.ts b/server/src/interfaces/telemetry.interface.ts deleted file mode 100644 index 688e52c21effa..0000000000000 --- a/server/src/interfaces/telemetry.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MetricOptions } from '@opentelemetry/api'; -import { ClassConstructor } from 'class-transformer'; - -export const ITelemetryRepository = 'ITelemetryRepository'; - -export interface MetricGroupOptions { - enabled: boolean; -} - -export interface IMetricGroupRepository { - addToCounter(name: string, value: number, options?: MetricOptions): void; - addToGauge(name: string, value: number, options?: MetricOptions): void; - addToHistogram(name: string, value: number, options?: MetricOptions): void; - configure(options: MetricGroupOptions): this; -} - -export interface ITelemetryRepository { - setup(options: { repositories: ClassConstructor[] }): void; - api: IMetricGroupRepository; - host: IMetricGroupRepository; - jobs: IMetricGroupRepository; - repo: IMetricGroupRepository; -} diff --git a/server/src/interfaces/trash.interface.ts b/server/src/interfaces/trash.interface.ts deleted file mode 100644 index 38e7c523ce613..0000000000000 --- a/server/src/interfaces/trash.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const ITrashRepository = 'ITrashRepository'; - -export interface ITrashRepository { - empty(userId: string): Promise; - restore(userId: string): Promise; - restoreAll(assetIds: string[]): Promise; - getDeletedIds(): AsyncIterableIterator<{ id: string }>; -} diff --git a/server/src/interfaces/version-history.interface.ts b/server/src/interfaces/version-history.interface.ts deleted file mode 100644 index c38552c24ff4e..0000000000000 --- a/server/src/interfaces/version-history.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { VersionHistoryEntity } from 'src/entities/version-history.entity'; - -export const IVersionHistoryRepository = 'IVersionHistoryRepository'; - -export interface IVersionHistoryRepository { - create(version: Omit): Promise; - getAll(): Promise; - getLatest(): Promise; -} diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 5895721b63d06..f363f2e91a3ed 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -4,10 +4,14 @@ import { InjectKysely } from 'nestjs-kysely'; import { AlbumsSharedUsersUsers, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; -import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface'; + +export type AlbumPermissionId = { + albumsId: string; + usersId: string; +}; @Injectable() -export class AlbumUserRepository implements IAlbumUserRepository { +export class AlbumUserRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) @@ -16,10 +20,7 @@ export class AlbumUserRepository implements IAlbumUserRepository { } @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) - update( - { usersId, albumsId }: AlbumPermissionId, - dto: Updateable, - ): Promise> { + update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable) { return this.db .updateTable('albums_shared_users_users') .set(dto) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 00fa130bb49ee..9c506197d6c1c 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -43,8 +43,8 @@ import { WithProperty, WithoutProperty, } from 'src/interfaces/asset.interface'; -import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface'; import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface'; +import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository'; import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database'; import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination'; diff --git a/server/src/repositories/cron.repository.ts b/server/src/repositories/cron.repository.ts index 12533f67af6ad..e6e8fe7568d29 100644 --- a/server/src/repositories/cron.repository.ts +++ b/server/src/repositories/cron.repository.ts @@ -1,11 +1,24 @@ import { Injectable } from '@nestjs/common'; import { SchedulerRegistry } from '@nestjs/schedule'; import { CronJob, CronTime } from 'cron'; -import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +type CronBase = { + name: string; + start?: boolean; +}; + +export type CronCreate = CronBase & { + expression: string; + onTick: () => void; +}; + +export type CronUpdate = CronBase & { + expression?: string; +}; + @Injectable() -export class CronRepository implements ICronRepository { +export class CronRepository { constructor( private schedulerRegistry: SchedulerRegistry, private logger: LoggingRepository, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7f4619d3de51e..b54c69e117bb6 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -1,33 +1,23 @@ -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; -import { INotificationRepository } from 'src/interfaces/notification.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -71,44 +61,44 @@ import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ AccessRepository, ActivityRepository, + AlbumUserRepository, AuditRepository, ApiKeyRepository, ConfigRepository, + CronRepository, LoggingRepository, + MapRepository, MediaRepository, MemoryRepository, + MetadataRepository, + NotificationRepository, + OAuthRepository, + ServerInfoRepository, + TelemetryRepository, + TrashRepository, ViewRepository, + VersionHistoryRepository, ]; export const providers = [ { provide: IAlbumRepository, useClass: AlbumRepository }, - { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, - { provide: ICronRepository, useClass: CronRepository }, { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IDatabaseRepository, useClass: DatabaseRepository }, { provide: IEventRepository, useClass: EventRepository }, { provide: IJobRepository, useClass: JobRepository }, { provide: ILibraryRepository, useClass: LibraryRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, - { provide: IMapRepository, useClass: MapRepository }, - { provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMoveRepository, useClass: MoveRepository }, - { provide: INotificationRepository, useClass: NotificationRepository }, - { provide: IOAuthRepository, useClass: OAuthRepository }, { provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPersonRepository, useClass: PersonRepository }, { provide: IProcessRepository, useClass: ProcessRepository }, { provide: ISearchRepository, useClass: SearchRepository }, - { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISessionRepository, useClass: SessionRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: IStackRepository, useClass: StackRepository }, { provide: IStorageRepository, useClass: StorageRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, - { provide: ITelemetryRepository, useClass: TelemetryRepository }, - { provide: ITrashRepository, useClass: TrashRepository }, { provide: IUserRepository, useClass: UserRepository }, - { provide: IVersionHistoryRepository, useClass: VersionHistoryRepository }, ]; diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 3c5c3f567113d..af24b0c94ea7f 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -11,24 +11,41 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; import { AssetEntity, withExif } from 'src/entities/asset.entity'; import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; import { LogLevel, SystemMetadataKey } from 'src/enum'; -import { - GeoPoint, - IMapRepository, - MapMarker, - MapMarkerSearchOptions, - ReverseGeocodeResult, -} from 'src/interfaces/map.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export interface MapMarkerSearchOptions { + isArchived?: boolean; + isFavorite?: boolean; + fileCreatedBefore?: Date; + fileCreatedAfter?: Date; +} + +export interface GeoPoint { + latitude: number; + longitude: number; +} + +export interface ReverseGeocodeResult { + country: string | null; + state: string | null; + city: string | null; +} + +export interface MapMarker extends ReverseGeocodeResult { + id: string; + lat: number; + lon: number; +} + interface MapDB extends DB { geodata_places_tmp: GeodataPlaces; naturalearth_countries_tmp: NaturalearthCountries; } @Injectable() -export class MapRepository implements IMapRepository { +export class MapRepository { constructor( private configRepository: ConfigRepository, @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository, diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 90a3a9e76538a..3f297d709b2fd 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -1,11 +1,72 @@ import { Injectable } from '@nestjs/common'; -import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored'; +import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored'; import geotz from 'geo-tz'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +interface ExifDuration { + Value: number; + Scale?: number; +} + +type StringOrNumber = string | number; + +type TagsWithWrongTypes = + | 'FocalLength' + | 'Duration' + | 'Description' + | 'ImageDescription' + | 'RegionInfo' + | 'TagsList' + | 'Keywords' + | 'HierarchicalSubject' + | 'ISO'; + +export interface ImmichTags extends Omit { + ContentIdentifier?: string; + MotionPhoto?: number; + MotionPhotoVersion?: number; + MotionPhotoPresentationTimestampUs?: number; + MediaGroupUUID?: string; + ImagePixelDepth?: string; + FocalLength?: number; + Duration?: number | string | ExifDuration; + EmbeddedVideoType?: string; + EmbeddedVideoFile?: BinaryField; + MotionPhotoVideo?: BinaryField; + TagsList?: StringOrNumber[]; + HierarchicalSubject?: StringOrNumber[]; + Keywords?: StringOrNumber | StringOrNumber[]; + ISO?: number | number[]; + + // Type is wrong, can also be number. + Description?: StringOrNumber; + ImageDescription?: StringOrNumber; + + // Extended properties for image regions, such as faces + RegionInfo?: { + AppliedToDimensions: { + W: number; + H: number; + Unit: string; + }; + RegionList: { + Area: { + // (X,Y) // center of the rectangle + X: number; + Y: number; + W: number; + H: number; + Unit: string; + }; + Rotation?: number; + Type?: string; + Name?: string; + }[]; + }; +} + @Injectable() -export class MetadataRepository implements IMetadataRepository { +export class MetadataRepository { private exiftool = new ExifTool({ defaultVideosToUTC: true, backfillTimezones: true, diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index f952e9ebedd09..0d8e826c66375 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -1,6 +1,5 @@ -import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; +import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; import { ILoggingRepository } from 'src/types'; import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { Mocked } from 'vitest'; diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index ecdaed38662b2..fdb74cfdb2c08 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -6,18 +6,104 @@ import { AlbumInviteEmail } from 'src/emails/album-invite.email'; import { AlbumUpdateEmail } from 'src/emails/album-update.email'; import { TestEmail } from 'src/emails/test.email'; import { WelcomeEmail } from 'src/emails/welcome.email'; -import { - EmailRenderRequest, - EmailTemplate, - INotificationRepository, - SendEmailOptions, - SendEmailResponse, - SmtpOptions, -} from 'src/interfaces/notification.interface'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export type EmailImageAttachment = { + filename: string; + path: string; + cid: string; +}; + +export type SendEmailOptions = { + from: string; + to: string; + replyTo?: string; + subject: string; + html: string; + text: string; + imageAttachments?: EmailImageAttachment[]; + smtp: SmtpOptions; +}; + +export type SmtpOptions = { + host: string; + port?: number; + username?: string; + password?: string; + ignoreCert?: boolean; +}; + +export enum EmailTemplate { + TEST_EMAIL = 'test', + + // AUTH + WELCOME = 'welcome', + RESET_PASSWORD = 'reset-password', + + // ALBUM + ALBUM_INVITE = 'album-invite', + ALBUM_UPDATE = 'album-update', +} + +interface BaseEmailProps { + baseUrl: string; + customTemplate?: string; +} + +export interface TestEmailProps extends BaseEmailProps { + displayName: string; +} + +export interface WelcomeEmailProps extends BaseEmailProps { + displayName: string; + username: string; + password?: string; +} + +export interface AlbumInviteEmailProps extends BaseEmailProps { + albumName: string; + albumId: string; + senderName: string; + recipientName: string; + cid?: string; +} + +export interface AlbumUpdateEmailProps extends BaseEmailProps { + albumName: string; + albumId: string; + recipientName: string; + cid?: string; +} + +export type EmailRenderRequest = + | { + template: EmailTemplate.TEST_EMAIL; + data: TestEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.WELCOME; + data: WelcomeEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.ALBUM_INVITE; + data: AlbumInviteEmailProps; + customTemplate: string; + } + | { + template: EmailTemplate.ALBUM_UPDATE; + data: AlbumUpdateEmailProps; + customTemplate: string; + }; + +export type SendEmailResponse = { + messageId: string; + response: any; +}; + @Injectable() -export class NotificationRepository implements INotificationRepository { +export class NotificationRepository { constructor(private logger: LoggingRepository) { this.logger.setContext(NotificationRepository.name); } diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index dfd36edc2a24d..85263cd6472b9 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -1,10 +1,21 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { custom, generators, Issuer } from 'openid-client'; -import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface'; +import { custom, generators, Issuer, UserinfoResponse } from 'openid-client'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export type OAuthConfig = { + clientId: string; + clientSecret: string; + issuerUrl: string; + mobileOverrideEnabled: boolean; + mobileRedirectUri: string; + profileSigningAlgorithm: string; + scope: string; + signingAlgorithm: string; +}; +export type OAuthProfile = UserinfoResponse; + @Injectable() -export class OAuthRepository implements IOAuthRepository { +export class OAuthRepository { constructor(private logger: LoggingRepository) { this.logger.setContext(OAuthRepository.name); } diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index c0c0f887231ba..deb24123d0514 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,10 +4,27 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; -import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +export interface GitHubRelease { + id: number; + url: string; + tag_name: string; + name: string; + created_at: string; + published_at: string; + body: string; +} + +export interface ServerBuildVersions { + nodejs: string; + ffmpeg: string; + libvips: string; + exiftool: string; + imagemagick: string; +} + const exec = promisify(execCallback); const maybeFirstLine = async (command: string): Promise => { try { @@ -34,7 +51,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => { }; @Injectable() -export class ServerInfoRepository implements IServerInfoRepository { +export class ServerInfoRepository { constructor( private configRepository: ConfigRepository, private logger: LoggingRepository, diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 7a82ba07e7409..7f93d4deba516 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -15,11 +15,12 @@ import { MetricService } from 'nestjs-otel'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; import { serverVersion } from 'src/constants'; import { ImmichTelemetry, MetadataKey } from 'src/enum'; -import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -class MetricGroupRepository implements IMetricGroupRepository { +type MetricGroupOptions = { enabled: boolean }; + +export class MetricGroupRepository { private enabled = false; constructor(private metricService: MetricService) {} @@ -86,7 +87,7 @@ export const teardownTelemetry = async () => { }; @Injectable() -export class TelemetryRepository implements ITelemetryRepository { +export class TelemetryRepository { api: MetricGroupRepository; host: MetricGroupRepository; jobs: MetricGroupRepository; diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts index 06e75a8d2e036..69507b1d583ff 100644 --- a/server/src/repositories/trash.repository.ts +++ b/server/src/repositories/trash.repository.ts @@ -3,9 +3,8 @@ import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetStatus } from 'src/enum'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; -export class TrashRepository implements ITrashRepository { +export class TrashRepository { constructor(@InjectKysely() private db: Kysely) {} getDeletedIds(): AsyncIterableIterator<{ id: string }> { diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts index e6ec8edcf4406..063ee0da84879 100644 --- a/server/src/repositories/version-history.repository.ts +++ b/server/src/repositories/version-history.repository.ts @@ -3,25 +3,23 @@ import { Insertable, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB, VersionHistory } from 'src/db'; import { GenerateSql } from 'src/decorators'; -import { VersionHistoryEntity } from 'src/entities/version-history.entity'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; @Injectable() -export class VersionHistoryRepository implements IVersionHistoryRepository { +export class VersionHistoryRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql() - getAll(): Promise { + getAll() { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute(); } @GenerateSql() - getLatest(): Promise { + getLatest() { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); } @GenerateSql({ params: [{ version: 'v1.123.0' }] }) - create(version: Insertable): Promise { + create(version: Insertable) { return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow(); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 99c794adc95d6..fe732843b6eb0 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -2,11 +2,11 @@ import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AlbumUserRole } from 'src/enum'; -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AlbumService } from 'src/services/album.service'; +import { IAlbumUserRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index ffa280677aac6..780d802922bc8 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -5,13 +5,12 @@ import { UserEntity } from 'src/entities/user.entity'; import { AuthType, Permission } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AuthService } from 'src/services/auth.service'; -import { IApiKeyRepository } from 'src/types'; +import { IApiKeyRepository, IOAuthRepository } from 'src/types'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { sessionStub } from 'test/fixtures/session.stub'; diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 4c0cdbab916ba..f46eb93111481 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -19,7 +19,7 @@ import { import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum'; -import { OAuthProfile } from 'src/interfaces/oauth.interface'; +import { OAuthProfile } from 'src/repositories/oauth.repository'; import { BaseService } from 'src/services/base.service'; import { AuthApiKey } from 'src/types'; import { isGranted } from 'src/utils/access'; diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 29adf9d8e1df6..33d77a59aae02 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -2,14 +2,13 @@ import { PassThrough } from 'node:stream'; import { defaults, SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { ImmichWorker, StorageFolder } from 'src/enum'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BackupService } from 'src/services/backup.service'; -import { IConfigRepository } from 'src/types'; +import { IConfigRepository, ICronRepository } from 'src/types'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { mockSpawn, newTestService } from 'test/utils'; import { describe, Mocked } from 'vitest'; diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 398b1508f5de8..865a16a9da936 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -6,44 +6,44 @@ import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; -import { INotificationRepository } from 'src/interfaces/notification.interface'; -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IProcessRepository } from 'src/interfaces/process.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -57,10 +57,10 @@ export class BaseService { protected activityRepository: ActivityRepository, protected auditRepository: AuditRepository, @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, - @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository, + protected albumUserRepository: AlbumUserRepository, @Inject(IAssetRepository) protected assetRepository: IAssetRepository, protected configRepository: ConfigRepository, - @Inject(ICronRepository) protected cronRepository: ICronRepository, + protected cronRepository: CronRepository, @Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository, @Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository, @Inject(IEventRepository) protected eventRepository: IEventRepository, @@ -68,28 +68,28 @@ export class BaseService { protected keyRepository: ApiKeyRepository, @Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository, @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository, - @Inject(IMapRepository) protected mapRepository: IMapRepository, + protected mapRepository: MapRepository, protected mediaRepository: MediaRepository, protected memoryRepository: MemoryRepository, - @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository, + protected metadataRepository: MetadataRepository, @Inject(IMoveRepository) protected moveRepository: IMoveRepository, - @Inject(INotificationRepository) protected notificationRepository: INotificationRepository, - @Inject(IOAuthRepository) protected oauthRepository: IOAuthRepository, + protected notificationRepository: NotificationRepository, + protected oauthRepository: OAuthRepository, @Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository, @Inject(IPersonRepository) protected personRepository: IPersonRepository, @Inject(IProcessRepository) protected processRepository: IProcessRepository, @Inject(ISearchRepository) protected searchRepository: ISearchRepository, - @Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository, + protected serverInfoRepository: ServerInfoRepository, @Inject(ISessionRepository) protected sessionRepository: ISessionRepository, @Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository, @Inject(IStackRepository) protected stackRepository: IStackRepository, @Inject(IStorageRepository) protected storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository, @Inject(ITagRepository) protected tagRepository: ITagRepository, - @Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository, - @Inject(ITrashRepository) protected trashRepository: ITrashRepository, + protected telemetryRepository: TelemetryRepository, + protected trashRepository: TrashRepository, @Inject(IUserRepository) protected userRepository: IUserRepository, - @Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository, + protected versionRepository: VersionHistoryRepository, protected viewRepository: ViewRepository, ) { this.logger.setContext(this.constructor.name); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 195ed1015608e..5d11f895a19c6 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -3,10 +3,10 @@ import { defaults, SystemConfig } from 'src/config'; import { ImmichWorker } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface'; -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; import { JobService } from 'src/services/job.service'; import { IConfigRepository, ILoggingRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; +import { ITelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; @@ -16,7 +16,7 @@ describe(JobService.name, () => { let configMock: Mocked; let jobMock: Mocked; let loggerMock: Mocked; - let telemetryMock: Mocked; + let telemetryMock: ITelemetryRepositoryMock; beforeEach(() => { ({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {})); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index e2d805b8652ed..5f81d92ec2b3d 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -5,7 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AssetType, ImmichWorker } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ICronRepository } from 'src/interfaces/cron.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, @@ -18,7 +17,7 @@ import { import { ILibraryRepository } from 'src/interfaces/library.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { LibraryService } from 'src/services/library.service'; -import { IConfigRepository } from 'src/types'; +import { IConfigRepository, ICronRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { libraryStub } from 'test/fixtures/library.stub'; diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index fde2ba7e0fe35..30505f7f5b844 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,7 +1,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { MapService } from 'src/services/map.service'; +import { IMapRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 4688e8b119a00..8cc6e014d2038 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -10,15 +10,14 @@ import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interfac import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { MetadataService } from 'src/services/metadata.service'; -import { IConfigRepository, IMediaRepository } from 'src/types'; +import { IConfigRepository, IMapRepository, IMediaRepository, IMetadataRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 406f80038c7ac..d5b7e6e4e4e8b 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -18,8 +18,8 @@ import { WithoutProperty } from 'src/interfaces/asset.interface'; import { DatabaseLock } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface'; -import { ReverseGeocodeResult } from 'src/interfaces/map.interface'; -import { ImmichTags } from 'src/interfaces/metadata.interface'; +import { ReverseGeocodeResult } from 'src/repositories/map.repository'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { BaseService } from 'src/services/base.service'; import { isFaceImportEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 76da12bbd6abb..671cae0774088 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -8,10 +8,11 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { EmailTemplate } from 'src/repositories/notification.repository'; import { NotificationService } from 'src/services/notification.service'; +import { INotificationRepository } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 37b265c6ae741..85f72443d4baa 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -12,7 +12,7 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface'; +import { EmailImageAttachment, EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index 4d877c9dfa469..8b93e899e7962 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; import { TrashService } from 'src/services/trash.service'; +import { ITrashRepository } from 'src/types'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newTestService } from 'test/utils'; diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 022e1de6139e6..406d3c1439377 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -4,11 +4,9 @@ import { serverVersion } from 'src/constants'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { VersionService } from 'src/services/version.service'; -import { IConfigRepository, ILoggingRepository } from 'src/types'; +import { IConfigRepository, ILoggingRepository, IServerInfoRepository, IVersionHistoryRepository } from 'src/types'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; diff --git a/server/src/types.ts b/server/src/types.ts index 69d9d8e647d7b..9928669136b73 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -2,12 +2,22 @@ import { UserEntity } from 'src/entities/user.entity'; import { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T; @@ -23,9 +33,11 @@ export type RepositoryInterface = Pick; export type IActivityRepository = RepositoryInterface; export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; +export type IAlbumUserRepository = RepositoryInterface; export type IApiKeyRepository = RepositoryInterface; export type IAuditRepository = RepositoryInterface; export type IConfigRepository = RepositoryInterface; +export type ICronRepository = RepositoryInterface; export type ILoggingRepository = Pick< LoggingRepository, | 'verbose' @@ -39,9 +51,18 @@ export type ILoggingRepository = Pick< | 'setContext' | 'setAppName' >; +export type IMapRepository = RepositoryInterface; export type IMediaRepository = RepositoryInterface; export type IMemoryRepository = RepositoryInterface; +export type IMetadataRepository = RepositoryInterface; +export type IMetricGroupRepository = RepositoryInterface; +export type INotificationRepository = RepositoryInterface; +export type IOAuthRepository = RepositoryInterface; +export type IServerInfoRepository = RepositoryInterface; +export type ITelemetryRepository = RepositoryInterface; +export type ITrashRepository = RepositoryInterface; export type IViewRepository = RepositoryInterface; +export type IVersionHistoryRepository = RepositoryInterface; export type ActivityItem = | Awaited> diff --git a/server/test/fixtures/metadata.stub.ts b/server/test/fixtures/metadata.stub.ts index 05535303e45a6..e60d8d0eac547 100644 --- a/server/test/fixtures/metadata.stub.ts +++ b/server/test/fixtures/metadata.stub.ts @@ -1,4 +1,4 @@ -import { ImmichTags } from 'src/interfaces/metadata.interface'; +import { ImmichTags } from 'src/repositories/metadata.repository'; import { personStub } from 'test/fixtures/person.stub'; export const metadataStub = { diff --git a/server/test/repositories/album-user.repository.mock.ts b/server/test/repositories/album-user.repository.mock.ts index 70c0487256da4..aa9436e33d5f3 100644 --- a/server/test/repositories/album-user.repository.mock.ts +++ b/server/test/repositories/album-user.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; +import { IAlbumUserRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newAlbumUserRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/cron.repository.mock.ts b/server/test/repositories/cron.repository.mock.ts index 2b0784e8ac27e..cc856909c8a9f 100644 --- a/server/test/repositories/cron.repository.mock.ts +++ b/server/test/repositories/cron.repository.mock.ts @@ -1,4 +1,4 @@ -import { ICronRepository } from 'src/interfaces/cron.interface'; +import { ICronRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newCronRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/map.repository.mock.ts b/server/test/repositories/map.repository.mock.ts index 703e8696f10d3..4b56b9443adcf 100644 --- a/server/test/repositories/map.repository.mock.ts +++ b/server/test/repositories/map.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMapRepository } from 'src/interfaces/map.interface'; +import { IMapRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newMapRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 60c5644b36673..e9bb68b95b982 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IMetadataRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newMetadataRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/notification.repository.mock.ts b/server/test/repositories/notification.repository.mock.ts index 16862dc3d762b..2065a0bf3e587 100644 --- a/server/test/repositories/notification.repository.mock.ts +++ b/server/test/repositories/notification.repository.mock.ts @@ -1,4 +1,4 @@ -import { INotificationRepository } from 'src/interfaces/notification.interface'; +import { INotificationRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newNotificationRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/oauth.repository.mock.ts b/server/test/repositories/oauth.repository.mock.ts index f87b3781e955f..8980bfb14f905 100644 --- a/server/test/repositories/oauth.repository.mock.ts +++ b/server/test/repositories/oauth.repository.mock.ts @@ -1,4 +1,4 @@ -import { IOAuthRepository } from 'src/interfaces/oauth.interface'; +import { IOAuthRepository } from 'src/types'; import { Mocked } from 'vitest'; export const newOAuthRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/server-info.repository.mock.ts b/server/test/repositories/server-info.repository.mock.ts index f55933d3c6742..5e9ecd1387bea 100644 --- a/server/test/repositories/server-info.repository.mock.ts +++ b/server/test/repositories/server-info.repository.mock.ts @@ -1,4 +1,4 @@ -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { IServerInfoRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newServerInfoRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/telemetry.repository.mock.ts b/server/test/repositories/telemetry.repository.mock.ts index 2d537e888af29..afadcea0cfbe2 100644 --- a/server/test/repositories/telemetry.repository.mock.ts +++ b/server/test/repositories/telemetry.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITelemetryRepository } from 'src/interfaces/telemetry.interface'; +import { ITelemetryRepository, RepositoryInterface } from 'src/types'; import { Mocked, vitest } from 'vitest'; const newMetricGroupMock = () => { @@ -10,7 +10,11 @@ const newMetricGroupMock = () => { }; }; -export const newTelemetryRepositoryMock = (): Mocked => { +export type ITelemetryRepositoryMock = { + [K in keyof ITelemetryRepository]: Mocked>; +}; + +export const newTelemetryRepositoryMock = (): ITelemetryRepositoryMock => { return { setup: vitest.fn(), api: newMetricGroupMock(), diff --git a/server/test/repositories/trash.repository.mock.ts b/server/test/repositories/trash.repository.mock.ts index 472b315b01410..f983afdce8b12 100644 --- a/server/test/repositories/trash.repository.mock.ts +++ b/server/test/repositories/trash.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { ITrashRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newTrashRepositoryMock = (): Mocked => { diff --git a/server/test/repositories/version-history.repository.mock.ts b/server/test/repositories/version-history.repository.mock.ts index 7c35e316d3315..9ff77087968dc 100644 --- a/server/test/repositories/version-history.repository.mock.ts +++ b/server/test/repositories/version-history.repository.mock.ts @@ -1,4 +1,4 @@ -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; +import { IVersionHistoryRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newVersionHistoryRepositoryMock = (): Mocked => { diff --git a/server/test/utils.ts b/server/test/utils.ts index 0ab1739e1433d..94377ca18c968 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -2,24 +2,42 @@ import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import { ImmichWorker } from 'src/enum'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { BaseService } from 'src/services/base.service'; import { IAccessRepository, IActivityRepository, + IAlbumUserRepository, IApiKeyRepository, IAuditRepository, + ICronRepository, ILoggingRepository, + IMapRepository, IMediaRepository, IMemoryRepository, + IMetadataRepository, + INotificationRepository, + IOAuthRepository, + IServerInfoRepository, + ITrashRepository, + IVersionHistoryRepository, IViewRepository, } from 'src/types'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; @@ -66,7 +84,7 @@ import { Mocked, vitest } from 'vitest'; type Overrides = { worker?: ImmichWorker; - metadataRepository?: IMetadataRepository; + metadataRepository?: MetadataRepository; }; type BaseServiceArgs = ConstructorParameters; type Constructor> = { @@ -125,10 +143,10 @@ export const newTestService = ( activityMock as IActivityRepository as ActivityRepository, auditMock as IAuditRepository as AuditRepository, albumMock, - albumUserMock, + albumUserMock as IAlbumUserRepository as AlbumUserRepository, assetMock, configMock, - cronMock, + cronMock as ICronRepository as CronRepository, cryptoMock, databaseMock, eventMock, @@ -136,28 +154,28 @@ export const newTestService = ( keyMock as IApiKeyRepository as ApiKeyRepository, libraryMock, machineLearningMock, - mapMock, + mapMock as IMapRepository as MapRepository, mediaMock as IMediaRepository as MediaRepository, memoryMock as IMemoryRepository as MemoryRepository, - metadataMock, + metadataMock as IMetadataRepository as MetadataRepository, moveMock, - notificationMock, - oauthMock, + notificationMock as INotificationRepository as NotificationRepository, + oauthMock as IOAuthRepository as OAuthRepository, partnerMock, personMock, processMock, searchMock, - serverInfoMock, + serverInfoMock as IServerInfoRepository as ServerInfoRepository, sessionMock, sharedLinkMock, stackMock, storageMock, systemMock, tagMock, - telemetryMock, - trashMock, + telemetryMock as unknown as TelemetryRepository, + trashMock as ITrashRepository as TrashRepository, userMock, - versionHistoryMock, + versionHistoryMock as IVersionHistoryRepository as VersionHistoryRepository, viewMock as IViewRepository as ViewRepository, ); From a07ae9b5b2dd8e747d1f620fdf5f5635277d5ee7 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:24:29 -0500 Subject: [PATCH 123/184] fix(server): set `updatedAt` on updates (#15573) * `updatedAt` triggers * drop function at the end --- .../1737672307560-AddUpdatedAtTriggers.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts diff --git a/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts b/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts new file mode 100644 index 0000000000000..74dde826fb7e4 --- /dev/null +++ b/server/src/migrations/1737672307560-AddUpdatedAtTriggers.ts @@ -0,0 +1,102 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUpdatedAtTriggers1737672307560 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + create function updated_at() + returns trigger as $$ + begin + new."updatedAt" = now(); + return new; + end; + $$ language 'plpgsql'`); + + await queryRunner.query(` + create trigger activity_updated_at + before update on activity + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger albums_updated_at + before update on albums + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger api_keys_updated_at + before update on api_keys + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger asset_files_updated_at + before update on asset_files + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger assets_updated_at + before update on assets + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger libraries_updated_at + before update on libraries + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger memories_updated_at + before update on memories + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger partners_updated_at + before update on partners + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger person_updated_at + before update on person + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger sessions_updated_at + before update on sessions + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger tags_updated_at + before update on tags + for each row execute procedure updated_at() + `); + + await queryRunner.query(` + create trigger users_updated_at + before update on users + for each row execute procedure updated_at() + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`drop trigger activity_updated_at on activity`); + await queryRunner.query(`drop trigger albums_updated_at on albums`); + await queryRunner.query(`drop trigger api_keys_updated_at on api_keys`); + await queryRunner.query(`drop trigger asset_files_updated_at on asset_files`); + await queryRunner.query(`drop trigger assets_updated_at on assets`); + await queryRunner.query(`drop trigger libraries_updated_at on libraries`); + await queryRunner.query(`drop trigger memories_updated_at on memories`); + await queryRunner.query(`drop trigger partners_updated_at on partners`); + await queryRunner.query(`drop trigger person_updated_at on person`); + await queryRunner.query(`drop trigger sessions_updated_at on sessions`); + await queryRunner.query(`drop trigger tags_updated_at on tags`); + await queryRunner.query(`drop trigger users_updated_at on users`); + await queryRunner.query(`drop function updated_at_trigger`); + } +} From 065d885ca0bb68e25c02aeb80e7536d81544926d Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 23 Jan 2025 21:33:24 -0500 Subject: [PATCH 124/184] fix(server): Fix for sorting faces during merging (#15571) * Fix for sorting faces * Put uneccessary orderBy in if statement --- server/src/repositories/person.repository.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index fdcecd9d0af93..45183f39d6fae 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -133,10 +133,6 @@ export class PersonRepository implements IPersonRepository { ) .where('person.ownerId', '=', userId) .orderBy('person.isHidden', 'asc') - .orderBy(sql`NULLIF(person.name, '') is null`, 'asc') - .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc') - .orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`) - .orderBy('person.createdAt') .having((eb) => eb.or([ eb('person.name', '!=', ''), @@ -161,6 +157,13 @@ export class PersonRepository implements IPersonRepository { ), ), ) + .$if(!options?.closestFaceAssetId, (qb) => + qb + .orderBy(sql`NULLIF(person.name, '') is null`, 'asc') + .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc') + .orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`) + .orderBy('person.createdAt'), + ) .$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false)) .offset(pagination.skip ?? 0) .limit(pagination.take + 1) From ba105d9f19c6c1102983f7a331ab851e346f3632 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:41:54 -0500 Subject: [PATCH 125/184] fix(server): `searchRandom` response (#15580) * fix searchRandom * add e2e * set outer limit --- e2e/src/api/specs/asset.e2e-spec.ts | 8 ++- e2e/src/api/specs/search.e2e-spec.ts | 53 +++++++++++++++++++- e2e/src/utils.ts | 1 + server/src/queries/search.repository.sql | 2 + server/src/repositories/search.repository.ts | 5 +- 5 files changed, 60 insertions(+), 9 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index ec09d71d21053..32cbdd6df812a 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -3,11 +3,11 @@ import { AssetMediaStatus, AssetResponseDto, AssetTypeEnum, - LoginResponseDto, - SharedLinkType, getAssetInfo, getConfig, getMyUser, + LoginResponseDto, + SharedLinkType, updateConfig, } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; @@ -19,7 +19,7 @@ import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -41,8 +41,6 @@ const makeUploadDto = (options?: { omit: string }): Record => { return dto; }; -const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 11bb37be1853b..50fce29ce016a 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -1,10 +1,10 @@ -import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk'; +import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk'; import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const today = DateTime.now(); @@ -462,6 +462,55 @@ describe('/search', () => { }); }); + describe('POST /search/random', () => { + beforeAll(async () => { + await Promise.all([ + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + ]); + + await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); + }); + + it('should require authentication', async () => { + const { status, body } = await request(app).post('/search/random').send({ size: 1 }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it.each(TEN_TIMES)('should return 1 random assets', async () => { + const { status, body } = await request(app) + .post('/search/random') + .send({ size: 1 }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(1); + expect(assets[0].ownerId).toBe(admin.userId); + }); + + it.each(TEN_TIMES)('should return 2 random assets', async () => { + const { status, body } = await request(app) + .post('/search/random') + .send({ size: 2 }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(2); + expect(assets[0].ownerId).toBe(admin.userId); + expect(assets[1].ownerId).toBe(admin.userId); + }); + }); + describe('GET /search/explore', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/search/explore'); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 7b80ba49aab28..efd9ce76b977f 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -76,6 +76,7 @@ export const immichCli = (args: string[]) => export const immichAdmin = (args: string[]) => executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; +export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const executeCommand = (command: string, args: string[]) => { let _resolve: (value: CommandResponse) => void; diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 1e509437812ec..2d5da4d381112 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -60,6 +60,8 @@ union all limit $14 ) +limit + $15 -- SearchRepository.searchSmart select diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 9abee70de3e21..fb59157c8024f 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -69,12 +69,13 @@ export class SearchRepository implements ISearchRepository { }, ], }) - searchRandom(size: number, options: AssetSearchOptions): Promise { + async searchRandom(size: number, options: AssetSearchOptions): Promise { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); const lessThan = builder.where('assets.id', '<', uuid).orderBy('assets.id').limit(size); const greaterThan = builder.where('assets.id', '>', uuid).orderBy('assets.id').limit(size); - return sql`${lessThan} union all ${greaterThan}`.execute(this.db) as any as Promise; + const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); + return rows as any as AssetEntity[]; } @GenerateSql({ From 8a481e2ea14b134d1165e768b2618f49b1e0dbd1 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 24 Jan 2025 16:08:01 +0100 Subject: [PATCH 126/184] docs: add FAQ about app update approval (#15599) --- docs/docs/FAQ.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index c605c564cd763..4cd3717e84e03 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -62,6 +62,10 @@ Instead of these experimental features, we recommend using the URL switching fea We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them. Please discuss any large PRs with our dev team to ensure your time is not wasted. +### Why isn't the mobile app updated yet? + +The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases. + --- ## Assets From 19740a35603156262087c090588c0d9dce89e8ad Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 09:18:26 -0600 Subject: [PATCH 127/184] fix(web): neon artifacts (#15582) --- web/src/app.css | 2 +- web/src/lib/components/layouts/AuthPageLayout.svelte | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/app.css b/web/src/app.css index 7a547d3504ee2..1127b60624cbb 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -15,7 +15,7 @@ /* dark */ --immich-dark-primary: 172 203 250; - --immich-dark-bg: 0 0 0; + --immich-dark-bg: 10 10 10; --immich-dark-fg: 229 231 235; --immich-dark-gray: 33 33 33; --immich-dark-error: 211 47 47; diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index f4532902c2590..51186f83b22ba 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -1,7 +1,7 @@
-
- Immich logo -
+
+ Immich logo +
From 6c95eb22b72754a5ad5c5d1c95acf66c8fc80a80 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:27:33 -0600 Subject: [PATCH 130/184] fix(mobile): full refresh doesn't get albums (#15560) --- mobile/lib/pages/photos/photos.page.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index 9e15b0193e749..845de40ee7a7f 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -83,11 +83,18 @@ class PhotosPage extends HookConsumerWidget { Future refreshAssets() async { final fullRefresh = refreshCount.value > 0; - await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh); + if (fullRefresh) { + Future.wait([ + ref.read(assetProvider.notifier).getAllAsset(clear: true), + ref.read(albumProvider.notifier).refreshRemoteAlbums(), + ]); + // refresh was forced: user requested another refresh within 2 seconds refreshCount.value = 0; } else { + await ref.read(assetProvider.notifier).getAllAsset(clear: false); + refreshCount.value++; // set counter back to 0 if user does not request refresh again Timer(const Duration(seconds: 4), () => refreshCount.value = 0); From 61bc24d7eacca0ae5f54b6ceb9f13ad93f7ff41e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:28:00 -0600 Subject: [PATCH 131/184] chore(mobile): post release task (#15581) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 10133cc330549..d7d24a9fa91ed 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 187; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 187; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 187; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +748,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -791,7 +791,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -831,7 +831,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 189; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index be5ec5d9d7069..a3b34a9bcdbaa 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.124.0 + 1.125.1 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 187 + 189 FLTEnableImpeller ITSAppUsesNonExemptEncryption From ec7ab209f3a9c56fb9a32c02496a384187f23d86 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:38:59 -0600 Subject: [PATCH 132/184] fix(server): link live photos (#15612) * fix(server): link live photos * chore: sql * formatting --- server/src/repositories/asset.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 9c506197d6c1c..6bb253d183dc9 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -430,9 +430,9 @@ export class AssetRepository implements IAssetRepository { findLivePhotoMatch(options: LivePhotoSearchOptions): Promise { const { ownerId, otherAssetId, livePhotoCID, type } = options; - return this.db .selectFrom('assets') + .select('assets.id') .innerJoin('exif', 'assets.id', 'exif.assetId') .where('id', '!=', asUuid(otherAssetId)) .where('ownerId', '=', asUuid(ownerId)) From ede9c99adbf1ee11bee565b6086ea6a9479b2c79 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 24 Jan 2025 12:39:06 -0500 Subject: [PATCH 133/184] fix: demo login page (#15616) --- web/src/routes/auth/login/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index e60cd5f145324..c3d01b3c56df2 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -116,11 +116,11 @@ {/if} - + - + From a6ace5151c11e6d85293188c30c08e54601b3879 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:42:39 -0500 Subject: [PATCH 134/184] fix(server): no exif metadata in the deduplication utility (#15585) add exif to `getDuplicates` --- server/src/queries/asset.repository.sql | 23 ++++++++++++++------ server/src/repositories/asset.repository.ts | 24 +++++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 67f9f39c8461a..948f7dd1143fa 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -306,17 +306,26 @@ order by with "duplicates" as ( select - "duplicateId", - jsonb_agg("assets") as "assets" + "assets"."duplicateId", + jsonb_agg("asset") as "assets" from "assets" + left join lateral ( + select + "assets".*, + "exif" as "exifInfo" + from + "exif" + where + "exif"."assetId" = "assets"."id" + ) as "asset" on true where - "ownerId" = $1::uuid - and "duplicateId" is not null - and "deletedAt" is null - and "isVisible" = $2 + "assets"."ownerId" = $1::uuid + and "assets"."duplicateId" is not null + and "assets"."deletedAt" is null + and "assets"."isVisible" = $2 group by - "duplicateId" + "assets"."duplicateId" ), "unique" as ( select diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 6bb253d183dc9..b39781209eade 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -677,13 +677,23 @@ export class AssetRepository implements IAssetRepository { .with('duplicates', (qb) => qb .selectFrom('assets') - .select('duplicateId') - .select((eb) => eb.fn('jsonb_agg', [eb.table('assets')]).as('assets')) - .where('ownerId', '=', asUuid(userId)) - .where('duplicateId', 'is not', null) - .where('deletedAt', 'is', null) - .where('isVisible', '=', true) - .groupBy('duplicateId'), + .leftJoinLateral( + (qb) => + qb + .selectFrom('exif') + .selectAll('assets') + .select((eb) => eb.table('exif').as('exifInfo')) + .whereRef('exif.assetId', '=', 'assets.id') + .as('asset'), + (join) => join.onTrue(), + ) + .select('assets.duplicateId') + .select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets')) + .where('assets.ownerId', '=', asUuid(userId)) + .where('assets.duplicateId', 'is not', null) + .where('assets.deletedAt', 'is', null) + .where('assets.isVisible', '=', true) + .groupBy('assets.duplicateId'), ) .with('unique', (qb) => qb From c0210bd6c08f5dedb57c27c023792ab2dd8d2add Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:47:01 -0500 Subject: [PATCH 135/184] fix(mobile): translation (no /api, experimental features) (#15600) * initial /api removal * translations /api * experimental features * japanese url update --------- Co-authored-by: Alex --- docs/docs/install/script.md | 2 +- docs/docs/partials/_mobile-app-login.md | 2 +- install.sh | 2 +- mobile/assets/i18n/ar-JO.json | 6 +++--- mobile/assets/i18n/ca-CA.json | 2 +- mobile/assets/i18n/cs-CZ.json | 2 +- mobile/assets/i18n/da-DK.json | 2 +- mobile/assets/i18n/de-DE.json | 2 +- mobile/assets/i18n/el-GR.json | 2 +- mobile/assets/i18n/en-US.json | 10 +++++----- mobile/assets/i18n/es-ES.json | 2 +- mobile/assets/i18n/es-MX.json | 2 +- mobile/assets/i18n/es-PE.json | 2 +- mobile/assets/i18n/es-US.json | 2 +- mobile/assets/i18n/fi-FI.json | 2 +- mobile/assets/i18n/fr-CA.json | 2 +- mobile/assets/i18n/fr-FR.json | 2 +- mobile/assets/i18n/he-IL.json | 6 +++--- mobile/assets/i18n/hi-IN.json | 2 +- mobile/assets/i18n/hu-HU.json | 4 ++-- mobile/assets/i18n/id-ID.json | 2 +- mobile/assets/i18n/it-IT.json | 2 +- mobile/assets/i18n/ja-JP.json | 4 ++-- mobile/assets/i18n/ko-KR.json | 2 +- mobile/assets/i18n/lt-LT.json | 2 +- mobile/assets/i18n/lv-LV.json | 2 +- mobile/assets/i18n/mn-MN.json | 2 +- mobile/assets/i18n/nb-NO.json | 2 +- mobile/assets/i18n/nl-NL.json | 2 +- mobile/assets/i18n/pl-PL.json | 2 +- mobile/assets/i18n/pt-BR.json | 2 +- mobile/assets/i18n/pt-PT.json | 2 +- mobile/assets/i18n/ro-RO.json | 2 +- mobile/assets/i18n/ru-RU.json | 2 +- mobile/assets/i18n/sk-SK.json | 2 +- mobile/assets/i18n/sl-SI.json | 2 +- mobile/assets/i18n/sr-Cyrl.json | 2 +- mobile/assets/i18n/sr-Latn.json | 2 +- mobile/assets/i18n/sv-FI.json | 2 +- mobile/assets/i18n/sv-SE.json | 2 +- mobile/assets/i18n/th-TH.json | 2 +- mobile/assets/i18n/tr-TR.json | 2 +- mobile/assets/i18n/uk-UA.json | 2 +- mobile/assets/i18n/vi-VN.json | 2 +- mobile/assets/i18n/zh-CN.json | 4 ++-- mobile/assets/i18n/zh-Hans.json | 4 ++-- mobile/assets/i18n/zh-TW.json | 4 ++-- .../networking_settings/local_network_preference.dart | 4 ++-- 48 files changed, 62 insertions(+), 62 deletions(-) diff --git a/docs/docs/install/script.md b/docs/docs/install/script.md index a515f2b628ff4..93d1fb166c066 100644 --- a/docs/docs/install/script.md +++ b/docs/docs/install/script.md @@ -27,7 +27,7 @@ The script will perform the following actions: 1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich). 2. Start the containers. -The web application will be available at `http://:2283`, and the server URL for the mobile app will be `http://:2283/api` +The web application and mobile app will be available at `http://:2283` The directory which is used to store the library files is `./immich-app` relative to the current directory. diff --git a/docs/docs/partials/_mobile-app-login.md b/docs/docs/partials/_mobile-app-login.md index bfd15ac5d0c53..3dc8f30933d4e 100644 --- a/docs/docs/partials/_mobile-app-login.md +++ b/docs/docs/partials/_mobile-app-login.md @@ -1,3 +1,3 @@ -Login to the mobile app with the server endpoint URL at `http://:2283/api` +Login to the mobile app with the server endpoint URL at `http://:2283` diff --git a/install.sh b/install.sh index e9c65b32836ff..d6569f736a04e 100755 --- a/install.sh +++ b/install.sh @@ -53,7 +53,7 @@ show_friendly_message() { ip_address=$(hostname -I | awk '{print $1}') cat < Date: Fri, 24 Jan 2025 18:47:54 +0100 Subject: [PATCH 136/184] fix(mobile): deletion of single assets (#15597) fix: set asset in currentassetprovider on image load Co-authored-by: Alex --- mobile/lib/pages/common/gallery_viewer.page.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 7e47c1d087efc..f51be027f562b 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -262,6 +262,11 @@ class GalleryViewerPage extends HookConsumerWidget { PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { var newAsset = loadAsset(index); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(currentAssetProvider.notifier).set(newAsset); + }); + final stackId = newAsset.stackId; if (stackId != null && currentIndex.value == index) { final stackElements = From 9d8072b994982d9389da703969f3786331c7c1ab Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:54:53 -0600 Subject: [PATCH 137/184] fix(server): failed to get albums with archived assets (#15611) * fix(mobile): failed to get albums with archived assets * sql --- server/src/queries/album.repository.sql | 3 +-- server/src/repositories/album.repository.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 8f171466338c0..48dc4dda4e5aa 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -98,7 +98,6 @@ select where "albums_assets_assets"."albumsId" = "albums"."id" and "assets"."deletedAt" is null - and "assets"."isArchived" = $1 order by "assets"."fileCreatedAt" desc ) as "asset" @@ -106,7 +105,7 @@ select from "albums" where - "albums"."id" = $2 + "albums"."id" = $1 and "albums"."deletedAt" is null -- AlbumRepository.getByAssetId diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index e32a53e82d2cd..d3b696169b748 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -63,7 +63,6 @@ const withAssets = (eb: ExpressionBuilder) => { .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) - .where('assets.isArchived', '=', false) .orderBy('assets.fileCreatedAt', 'desc') .as('asset'), ) From d4a9eed4a180e6a17f6c5e107175f8db4f995a12 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 12:11:22 -0600 Subject: [PATCH 138/184] fix(server): migration mentions public schema (#15622) --- server/src/migrations/1734574016301-AddTimeBucketIndices.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/migrations/1734574016301-AddTimeBucketIndices.ts b/server/src/migrations/1734574016301-AddTimeBucketIndices.ts index 71e085ee18d93..2162a713fcc2c 100644 --- a/server/src/migrations/1734574016301-AddTimeBucketIndices.ts +++ b/server/src/migrations/1734574016301-AddTimeBucketIndices.ts @@ -3,10 +3,10 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddTimeBucketIndices1734574016301 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE INDEX idx_local_date_time_month ON public.assets ((date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC'))`, + `CREATE INDEX idx_local_date_time_month ON assets ((date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC'))`, ); await queryRunner.query( - `CREATE INDEX idx_local_date_time ON public.assets ((("localDateTime" at time zone 'UTC')::date))`, + `CREATE INDEX idx_local_date_time ON assets ((("localDateTime" at time zone 'UTC')::date))`, ); await queryRunner.query(`DROP INDEX "IDX_day_of_month"`); await queryRunner.query(`DROP INDEX "IDX_month"`); From f5a3d7ba232d77ab55b7202deb15404395ef7422 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 12:47:29 -0600 Subject: [PATCH 139/184] fix(mobile): failed to load ga/gl locale (#15623) --- localizely.yml | 4 +- mobile/assets/i18n/ga.json | 673 +++++++++++++++++++++++++++++++++++++ mobile/assets/i18n/gl.json | 673 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1348 insertions(+), 2 deletions(-) create mode 100644 mobile/assets/i18n/ga.json create mode 100644 mobile/assets/i18n/gl.json diff --git a/localizely.yml b/localizely.yml index 2a9b95464fd71..0b8309ea6d484 100644 --- a/localizely.yml +++ b/localizely.yml @@ -97,8 +97,8 @@ download: - file: mobile/assets/i18n/id-ID.json locale_code: id-ID - file: mobile/assets/i18n/gl.json - locale_code: gl-ES + locale_code: gl - file: mobile/assets/i18n/ga.json - locale_code: ga-IE + locale_code: ga - file: mobile/assets/i18n/tr-TR.json locale_code: tr-TR diff --git a/mobile/assets/i18n/ga.json b/mobile/assets/i18n/ga.json new file mode 100644 index 0000000000000..9450b4b44f2b3 --- /dev/null +++ b/mobile/assets/i18n/ga.json @@ -0,0 +1,673 @@ +{ + "action_common_back": "Back", + "action_common_cancel": "Cancel", + "action_common_clear": "Clear", + "action_common_confirm": "Confirm", + "action_common_save": "Save", + "action_common_select": "Select", + "action_common_update": "Update", + "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", + "add_to_album_bottom_sheet_added": "Added to {album}", + "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", + "advanced_settings_prefer_remote_title": "Prefer remote images", + "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", + "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", + "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates (EXPERIMENTAL)", + "advanced_settings_tile_subtitle": "Advanced user's settings", + "advanced_settings_tile_title": "Advanced", + "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", + "advanced_settings_troubleshooting_title": "Troubleshooting", + "album_info_card_backup_album_excluded": "EXCLUDED", + "album_info_card_backup_album_included": "INCLUDED", + "albums": "Albums", + "album_thumbnail_card_item": "1 item", + "album_thumbnail_card_items": "{} items", + "album_thumbnail_card_shared": " · Shared", + "album_thumbnail_owned": "Owned", + "album_thumbnail_shared_by": "Shared by {}", + "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", + "album_viewer_appbar_share_delete": "Delete album", + "album_viewer_appbar_share_err_delete": "Failed to delete album", + "album_viewer_appbar_share_err_leave": "Failed to leave album", + "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", + "album_viewer_appbar_share_err_title": "Failed to change album title", + "album_viewer_appbar_share_leave": "Leave album", + "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", + "album_viewer_page_share_add_users": "Add users", + "all": "All", + "all_people_page_title": "People", + "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", + "archived": "Archived", + "archive_page_no_archived_assets": "No archived assets found", + "archive_page_title": "Archive ({})", + "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", + "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_list_group_by_sub_title": "Group by", + "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", + "asset_list_layout_settings_group_automatically": "Automatic", + "asset_list_layout_settings_group_by": "Group assets by", + "asset_list_layout_settings_group_by_month": "Month", + "asset_list_layout_settings_group_by_month_day": "Month + day", + "asset_list_layout_sub_title": "Layout", + "asset_list_settings_subtitle": "Photo grid layout settings", + "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "backup_album_selection_page_albums_device": "Albums on device ({})", + "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", + "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", + "backup_album_selection_page_select_albums": "Select albums", + "backup_album_selection_page_selection_info": "Selection Info", + "backup_album_selection_page_total_assets": "Total unique assets", + "backup_all": "All", + "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", + "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…", + "backup_background_service_current_upload_notification": "Uploading {}", + "backup_background_service_default_notification": "Checking for new assets…", + "backup_background_service_error_title": "Backup error", + "backup_background_service_in_progress_notification": "Backing up your assets…", + "backup_background_service_upload_failure_notification": "Failed to upload {}", + "backup_controller_page_albums": "Backup Albums", + "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", + "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", + "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", + "backup_controller_page_background_battery_info_link": "Show me how", + "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", + "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_battery_info_title": "Battery optimizations", + "backup_controller_page_background_charging": "Only while charging", + "backup_controller_page_background_configure_error": "Failed to configure the background service", + "backup_controller_page_background_delay": "Delay new assets backup: {}", + "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", + "backup_controller_page_background_is_off": "Automatic background backup is off", + "backup_controller_page_background_is_on": "Automatic background backup is on", + "backup_controller_page_background_turn_off": "Turn off background service", + "backup_controller_page_background_turn_on": "Turn on background service", + "backup_controller_page_background_wifi": "Only on WiFi", + "backup_controller_page_backup": "Backup", + "backup_controller_page_backup_selected": "Selected: ", + "backup_controller_page_backup_sub": "Backed up photos and videos", + "backup_controller_page_cancel": "Cancel", + "backup_controller_page_created": "Created on: {}", + "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", + "backup_controller_page_excluded": "Excluded: ", + "backup_controller_page_failed": "Failed ({})", + "backup_controller_page_filename": "File name: {} [{}]", + "backup_controller_page_id": "ID: {}", + "backup_controller_page_info": "Backup Information", + "backup_controller_page_none_selected": "None selected", + "backup_controller_page_remainder": "Remainder", + "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", + "backup_controller_page_select": "Select", + "backup_controller_page_server_storage": "Server Storage", + "backup_controller_page_start_backup": "Start Backup", + "backup_controller_page_status_off": "Automatic foreground backup is off", + "backup_controller_page_status_on": "Automatic foreground backup is on", + "backup_controller_page_storage_format": "{} of {} used", + "backup_controller_page_to_backup": "Albums to be backed up", + "backup_controller_page_total": "Total", + "backup_controller_page_total_sub": "All unique photos and videos from selected albums", + "backup_controller_page_turn_off": "Turn off foreground backup", + "backup_controller_page_turn_on": "Turn on foreground backup", + "backup_controller_page_uploading_file_info": "Uploading file info", + "backup_err_only_album": "Cannot remove the only album", + "backup_info_card_assets": "assets", + "backup_manual_cancelled": "Cancelled", + "backup_manual_failed": "Failed", + "backup_manual_in_progress": "Upload already in progress. Try after sometime", + "backup_manual_success": "Success", + "backup_manual_title": "Upload status", + "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", + "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", + "cache_settings_clear_cache_button": "Clear cache", + "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", + "cache_settings_duplicated_assets_clear_button": "CLEAR", + "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", + "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_image_cache_size": "Image cache size ({} assets)", + "cache_settings_statistics_album": "Library thumbnails", + "cache_settings_statistics_assets": "{} assets ({})", + "cache_settings_statistics_full": "Full images", + "cache_settings_statistics_shared": "Shared album thumbnails", + "cache_settings_statistics_thumbnail": "Thumbnails", + "cache_settings_statistics_title": "Cache usage", + "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", + "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", + "cache_settings_title": "Caching Settings", + "cancel": "Cancel", + "canceled": "Canceled", + "change_display_order": "Change display order", + "change_password_form_confirm_password": "Confirm Password", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_new_password": "New Password", + "change_password_form_password_mismatch": "Passwords do not match", + "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Enter Password", + "client_cert_import": "Import", + "client_cert_import_success_msg": "Client certificate is imported", + "client_cert_invalid_msg": "Invalid certificate file or wrong password", + "client_cert_remove": "Remove", + "client_cert_remove_msg": "Client certificate is removed", + "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", + "client_cert_title": "SSL Client Certificate (EXPERIMENTAL)", + "common_add_to_album": "Add to album", + "common_change_password": "Change Password", + "common_create_new_album": "Create new album", + "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", + "common_shared": "Shared", + "completed": "Completed", + "contextual_search": "Sunrise on the beach", + "control_bottom_app_bar_add_to_album": "Add to album", + "control_bottom_app_bar_album_info": "{} items", + "control_bottom_app_bar_album_info_shared": "{} items · Shared", + "control_bottom_app_bar_archive": "Archive", + "control_bottom_app_bar_create_new_album": "Create new album", + "control_bottom_app_bar_delete": "Delete", + "control_bottom_app_bar_delete_from_immich": "Delete from Immich", + "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_download": "Download", + "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit_location": "Edit Location", + "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_favorite": "Favorite", + "control_bottom_app_bar_share": "Share", + "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_unarchive": "Unarchive", + "control_bottom_app_bar_unfavorite": "Unfavorite", + "control_bottom_app_bar_upload": "Upload", + "create_album": "Create album", + "create_album_page_untitled": "Untitled", + "create_new": "CREATE NEW", + "create_shared_album_page_create": "Create", + "create_shared_album_page_share": "Share", + "create_shared_album_page_share_add_assets": "ADD ASSETS", + "create_shared_album_page_share_select_photos": "Select Photos", + "crop": "Crop", + "curated_location_page_title": "Places", + "curated_object_page_title": "Things", + "current_server_address": "Current server address", + "daily_title_text_date": "E, MMM dd", + "daily_title_text_date_year": "E, MMM dd, yyyy", + "date_format": "E, LLL d, y • h:mm a", + "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", + "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", + "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", + "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_cancel": "Cancel", + "delete_dialog_ok": "Delete", + "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_title": "Delete Permanently", + "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", + "delete_local_dialog_ok_force": "Delete Anyway", + "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", + "delete_shared_link_dialog_title": "Delete Shared Link", + "description_input_hint_text": "Add description...", + "description_input_submit_error": "Error updating description, check the log for more details", + "download_canceled": "Download canceled", + "download_complete": "Download complete", + "download_enqueue": "Download enqueued", + "download_error": "Download Error", + "download_failed": "Download failed", + "download_filename": "file: {}", + "download_finished": "Download finished", + "downloading": "Downloading...", + "downloading_media": "Downloading media", + "download_notfound": "Download not found", + "download_paused": "Download paused", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", + "download_waiting_to_retry": "Waiting to retry", + "edit_date_time_dialog_date_time": "Date and Time", + "edit_date_time_dialog_timezone": "Timezone", + "edit_image_title": "Edit", + "edit_location_dialog_title": "Location", + "end_date": "End date", + "enqueued": "Enqueued", + "enter_wifi_name": "Enter WiFi name", + "error_change_sort_album": "Failed to change album sort order", + "error_saving_image": "Error: {}", + "exif_bottom_sheet_description": "Add Description...", + "exif_bottom_sheet_details": "DETAILS", + "exif_bottom_sheet_location": "LOCATION", + "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", + "exif_bottom_sheet_person_add_person": "Add name", + "experimental_settings_new_asset_list_subtitle": "Work in progress", + "experimental_settings_new_asset_list_title": "Enable experimental photo grid", + "experimental_settings_subtitle": "Use at your own risk!", + "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "failed": "Failed", + "favorites": "Favorites", + "favorites_page_no_favorites": "No favorite assets found", + "favorites_page_title": "Favorites", + "filename_search": "File name or extension", + "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", + "haptic_feedback_switch": "Enable haptic feedback", + "haptic_feedback_title": "Haptic Feedback", + "header_settings_add_header_tip": "Add Header", + "header_settings_field_validator_msg": "Value cannot be empty", + "header_settings_header_name_input": "Header name", + "header_settings_header_value_input": "Header value", + "header_settings_page_title": "Proxy Headers (EXPERIMENTAL)", + "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", + "headers_settings_tile_title": "Custom proxy headers", + "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", + "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", + "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", + "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "ignore_icloud_photos": "Ignore iCloud photos", + "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", + "image_saved_successfully": "Image saved", + "image_viewer_page_state_provider_download_error": "Download Error", + "image_viewer_page_state_provider_download_started": "Download Started", + "image_viewer_page_state_provider_download_success": "Download Success", + "image_viewer_page_state_provider_share_error": "Share Error", + "invalid_date": "Invalid date", + "invalid_date_format": "Invalid date format", + "library": "Library", + "library_page_albums": "Albums", + "library_page_archive": "Archive", + "library_page_device_albums": "Albums on Device", + "library_page_favorites": "Favorites", + "library_page_new_album": "New album", + "library_page_sharing": "Sharing", + "library_page_sort_asset_count": "Number of assets", + "library_page_sort_created": "Created date", + "library_page_sort_last_modified": "Last modified", + "library_page_sort_most_oldest_photo": "Oldest photo", + "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "location_picker_choose_on_map": "Choose on map", + "location_picker_latitude": "Latitude", + "location_picker_latitude_error": "Enter a valid latitude", + "location_picker_latitude_hint": "Enter your latitude here", + "location_picker_longitude": "Longitude", + "location_picker_longitude_error": "Enter a valid longitude", + "location_picker_longitude_hint": "Enter your longitude here", + "login_disabled": "Login has been disabled", + "login_form_api_exception": "API exception. Please check the server URL and try again.", + "login_form_back_button_text": "Back", + "login_form_button_text": "Login", + "login_form_email_hint": "youremail@email.com", + "login_form_endpoint_hint": "http://your-server-ip:port", + "login_form_endpoint_url": "Server Endpoint URL", + "login_form_err_http": "Please specify http:// or https://", + "login_form_err_invalid_email": "Invalid Email", + "login_form_err_invalid_url": "Invalid URL", + "login_form_err_leading_whitespace": "Leading whitespace", + "login_form_err_trailing_whitespace": "Trailing whitespace", + "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", + "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", + "login_form_failed_login": "Error logging you in, check server URL, email and password", + "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_label_email": "Email", + "login_form_label_password": "Password", + "login_form_next_button": "Next", + "login_form_password_hint": "password", + "login_form_save_login": "Stay logged in", + "login_form_server_empty": "Enter a server URL.", + "login_form_server_error": "Could not connect to server.", + "login_password_changed_error": "There was an error updating your password", + "login_password_changed_success": "Password updated successfully", + "map_assets_in_bound": "{} photo", + "map_assets_in_bounds": "{} photos", + "map_cannot_get_user_location": "Cannot get user's location", + "map_location_dialog_cancel": "Cancel", + "map_location_dialog_yes": "Yes", + "map_location_picker_page_use_location": "Use this location", + "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", + "map_location_service_disabled_title": "Location Service disabled", + "map_no_assets_in_bounds": "No photos in this area", + "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", + "map_no_location_permission_title": "Location Permission denied", + "map_settings_dark_mode": "Dark mode", + "map_settings_date_range_option_all": "All", + "map_settings_date_range_option_day": "Past 24 hours", + "map_settings_date_range_option_days": "Past {} days", + "map_settings_date_range_option_year": "Past year", + "map_settings_date_range_option_years": "Past {} years", + "map_settings_dialog_cancel": "Cancel", + "map_settings_dialog_save": "Save", + "map_settings_dialog_title": "Map Settings", + "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_partners": "Include Partners", + "map_settings_only_relative_range": "Date range", + "map_settings_only_show_favorites": "Show Favorite Only", + "map_settings_theme_settings": "Map Theme", + "map_zoom_to_see_photos": "Zoom out to see photos", + "memories_all_caught_up": "All caught up", + "memories_check_back_tomorrow": "Check back tomorrow for more memories", + "memories_start_over": "Start Over", + "memories_swipe_to_close": "Swipe up to close", + "memories_year_ago": "A year ago", + "memories_years_ago": "{} years ago", + "monthly_title_text_date_format": "MMMM y", + "motion_photos_page_title": "Motion Photos", + "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", + "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", + "no_assets_to_show": "No assets to show", + "no_name": "No name", + "notification_permission_dialog_cancel": "Cancel", + "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", + "notification_permission_dialog_settings": "Settings", + "notification_permission_list_tile_content": "Grant permission to enable notifications.", + "notification_permission_list_tile_enable_button": "Enable Notifications", + "notification_permission_list_tile_title": "Notification Permission", + "not_selected": "Not selected", + "on_this_device": "On this device", + "partner_list_user_photos": "{user}'s photos", + "partner_list_view_all": "View all", + "partner_page_add_partner": "Add partner", + "partner_page_empty_message": "Your photos are not yet shared with any partner.", + "partner_page_no_more_users": "No more users to add", + "partner_page_partner_add_failed": "Failed to add partner", + "partner_page_select_partner": "Select partner", + "partner_page_shared_to_title": "Shared to", + "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", + "partner_page_stop_sharing_title": "Stop sharing your photos?", + "partner_page_title": "Partner", + "partners": "Partners", + "paused": "Paused", + "people": "People", + "permission_onboarding_back": "Back", + "permission_onboarding_continue_anyway": "Continue anyway", + "permission_onboarding_get_started": "Get started", + "permission_onboarding_go_to_settings": "Go to settings", + "permission_onboarding_grant_permission": "Grant permission", + "permission_onboarding_log_out": "Log out", + "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", + "permission_onboarding_permission_granted": "Permission granted! You are all set.", + "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", + "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", + "preferences_settings_title": "Preferences", + "profile_drawer_app_logs": "Logs", + "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", + "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", + "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_settings": "Settings", + "profile_drawer_sign_out": "Sign Out", + "profile_drawer_trash": "Trash", + "recently_added": "Recently added", + "recently_added_page_title": "Recently Added", + "save": "Save", + "save_to_gallery": "Save to gallery", + "scaffold_body_error_occurred": "Error occurred", + "search_albums": "Search albums", + "search_bar_hint": "Search your photos", + "search_filter_apply": "Apply filter", + "search_filter_camera": "Camera", + "search_filter_camera_make": "Make", + "search_filter_camera_model": "Model", + "search_filter_camera_title": "Select camera type", + "search_filter_date": "Date", + "search_filter_date_interval": "{start} to {end}", + "search_filter_date_title": "Select a date range", + "search_filter_display_option_archive": "Archive", + "search_filter_display_option_favorite": "Favorite", + "search_filter_display_option_not_in_album": "Not in album", + "search_filter_display_options": "Display Options", + "search_filter_display_options_title": "Display options", + "search_filter_location": "Location", + "search_filter_location_city": "City", + "search_filter_location_country": "Country", + "search_filter_location_state": "State", + "search_filter_location_title": "Select location", + "search_filter_media_type": "Media Type", + "search_filter_media_type_all": "All", + "search_filter_media_type_image": "Image", + "search_filter_media_type_title": "Select media type", + "search_filter_media_type_video": "Video", + "search_filter_people": "People", + "search_filter_people_title": "Select people", + "search_page_categories": "Categories", + "search_page_favorites": "Favorites", + "search_page_motion_photos": "Motion Photos", + "search_page_no_objects": "No Objects Info Available", + "search_page_no_places": "No Places Info Available", + "search_page_people": "People", + "search_page_person_add_name_dialog_cancel": "Cancel", + "search_page_person_add_name_dialog_hint": "Name", + "search_page_person_add_name_dialog_save": "Save", + "search_page_person_add_name_dialog_title": "Add a name", + "search_page_person_add_name_subtitle": "Find them fast by name with search", + "search_page_person_add_name_title": "Add a name", + "search_page_person_edit_name": "Edit name", + "search_page_places": "Places", + "search_page_recently_added": "Recently added", + "search_page_screenshots": "Screenshots", + "search_page_search_photos_videos": "Search for your photos and videos", + "search_page_selfies": "Selfies", + "search_page_things": "Things", + "search_page_videos": "Videos", + "search_page_view_all_button": "View all", + "search_page_your_activity": "Your activity", + "search_page_your_map": "Your Map", + "search_result_page_new_search_hint": "New Search", + "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", + "search_suggestion_list_smart_search_hint_2": "m:your-search-term", + "select_additional_user_for_sharing_page_suggestions": "Suggestions", + "select_user_for_sharing_page_err_album": "Failed to create album", + "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", + "server_info_box_app_version": "App Version", + "server_info_box_latest_release": "Latest Version", + "server_info_box_server_url": "Server URL", + "server_info_box_server_version": "Server Version", + "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", + "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", + "setting_image_viewer_original_title": "Load original image", + "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", + "setting_image_viewer_preview_title": "Load preview image", + "setting_image_viewer_title": "Images", + "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", + "setting_languages_title": "Languages", + "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", + "setting_notifications_notify_hours": "{} hours", + "setting_notifications_notify_immediately": "immediately", + "setting_notifications_notify_minutes": "{} minutes", + "setting_notifications_notify_never": "never", + "setting_notifications_notify_seconds": "{} seconds", + "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", + "setting_notifications_single_progress_title": "Show background backup detail progress", + "setting_notifications_subtitle": "Adjust your notification preferences", + "setting_notifications_title": "Notifications", + "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", + "setting_notifications_total_progress_title": "Show background backup total progress", + "setting_pages_app_bar_settings": "Settings", + "settings_require_restart": "Please restart Immich to apply this setting", + "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", + "setting_video_viewer_looping_title": "Looping", + "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", + "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_title": "Videos", + "share_add": "Add", + "share_add_photos": "Add photos", + "share_add_title": "Add a title", + "share_assets_selected": "{} selected", + "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", + "shared_album_section_people_action_error": "Error leaving/removing from album", + "shared_album_section_people_action_leave": "Remove user from album", + "shared_album_section_people_action_remove_user": "Remove user from album", + "shared_album_section_people_owner_label": "Owner", + "shared_album_section_people_title": "PEOPLE", + "share_dialog_preparing": "Preparing...", + "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_link_app_bar_title": "Shared Links", + "shared_link_clipboard_copied_massage": "Copied to clipboard", + "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_create_app_bar_title": "Create link to share", + "shared_link_create_error": "Error while creating shared link", + "shared_link_create_info": "Let anyone with the link see the selected photo(s)", + "shared_link_create_submit_button": "Create link", + "shared_link_edit_allow_download": "Allow public user to download", + "shared_link_edit_allow_upload": "Allow public user to upload", + "shared_link_edit_app_bar_title": "Edit link", + "shared_link_edit_change_expiry": "Change expiration time", + "shared_link_edit_description": "Description", + "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_expire_after_option_day": "1 day", + "shared_link_edit_expire_after_option_days": "{} days", + "shared_link_edit_expire_after_option_hour": "1 hour", + "shared_link_edit_expire_after_option_hours": "{} hours", + "shared_link_edit_expire_after_option_minute": "1 minute", + "shared_link_edit_expire_after_option_minutes": "{} minutes", + "shared_link_edit_expire_after_option_months": "{} months", + "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_year": "{} year", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", + "shared_link_edit_show_meta": "Show metadata", + "shared_link_edit_submit_button": "Update link", + "shared_link_empty": "You don't have any shared links", + "shared_link_error_server_url_fetch": "Cannot fetch the server url", + "shared_link_expired": "Expired", + "shared_link_expires_day": "Expires in {} day", + "shared_link_expires_days": "Expires in {} days", + "shared_link_expires_hour": "Expires in {} hour", + "shared_link_expires_hours": "Expires in {} hours", + "shared_link_expires_minute": "Expires in {} minute", + "shared_link_expires_minutes": "Expires in {} minutes", + "shared_link_expires_never": "Expires ∞", + "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_seconds": "Expires in {} seconds", + "shared_link_individual_shared": "Individual shared", + "shared_link_info_chip_download": "Download", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Upload", + "shared_link_manage_links": "Manage Shared links", + "shared_link_public_album": "Public album", + "shared_links": "Shared links", + "share_done": "Done", + "shared_with_me": "Shared with me", + "share_invite": "Invite to album", + "sharing_page_album": "Shared albums", + "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", + "sharing_page_empty_list": "EMPTY LIST", + "sharing_silver_appbar_create_shared_album": "New shared album", + "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_share_partner": "Share with partner", + "start_date": "Start date", + "sync": "Sync", + "sync_albums": "Sync albums", + "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", + "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "tab_controller_nav_library": "Library", + "tab_controller_nav_photos": "Photos", + "tab_controller_nav_search": "Search", + "tab_controller_nav_sharing": "Sharing", + "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", + "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_dark_mode_switch": "Dark mode", + "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", + "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", + "theme_setting_system_theme_switch": "Automatic (Follow system setting)", + "theme_setting_theme_subtitle": "Choose the app's theme setting", + "theme_setting_theme_title": "Theme", + "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", + "theme_setting_three_stage_loading_title": "Enable three-stage loading", + "translated_text_options": "Options", + "trash": "Trash", + "trash_emptied": "Emptied trash", + "trash_page_delete": "Delete", + "trash_page_delete_all": "Delete All", + "trash_page_empty_trash_btn": "Empty trash", + "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_info": "Trashed items will be permanently deleted after {} days", + "trash_page_no_assets": "No trashed assets", + "trash_page_restore": "Restore", + "trash_page_restore_all": "Restore All", + "trash_page_select_assets_btn": "Select assets", + "trash_page_select_btn": "Select", + "trash_page_title": "Trash ({})", + "upload": "Upload", + "upload_dialog_cancel": "Cancel", + "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", + "upload_dialog_ok": "Upload", + "upload_dialog_title": "Upload Asset", + "uploading": "Uploading", + "upload_to_immich": "Upload to Immich ({})", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", + "version_announcement_overlay_ack": "Acknowledge", + "version_announcement_overlay_release_notes": "release notes", + "version_announcement_overlay_text_1": "Hi friend, there is a new release of", + "version_announcement_overlay_text_2": "please take your time to visit the ", + "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", + "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "videos": "Videos", + "viewer_remove_from_stack": "Remove from Stack", + "viewer_stack_use_as_main_asset": "Use as Main Asset", + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" +} diff --git a/mobile/assets/i18n/gl.json b/mobile/assets/i18n/gl.json new file mode 100644 index 0000000000000..9450b4b44f2b3 --- /dev/null +++ b/mobile/assets/i18n/gl.json @@ -0,0 +1,673 @@ +{ + "action_common_back": "Back", + "action_common_cancel": "Cancel", + "action_common_clear": "Clear", + "action_common_confirm": "Confirm", + "action_common_save": "Save", + "action_common_select": "Select", + "action_common_update": "Update", + "add_a_name": "Add a name", + "add_endpoint": "Add endpoint", + "add_to_album_bottom_sheet_added": "Added to {album}", + "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", + "advanced_settings_prefer_remote_title": "Prefer remote images", + "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", + "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", + "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates (EXPERIMENTAL)", + "advanced_settings_tile_subtitle": "Advanced user's settings", + "advanced_settings_tile_title": "Advanced", + "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", + "advanced_settings_troubleshooting_title": "Troubleshooting", + "album_info_card_backup_album_excluded": "EXCLUDED", + "album_info_card_backup_album_included": "INCLUDED", + "albums": "Albums", + "album_thumbnail_card_item": "1 item", + "album_thumbnail_card_items": "{} items", + "album_thumbnail_card_shared": " · Shared", + "album_thumbnail_owned": "Owned", + "album_thumbnail_shared_by": "Shared by {}", + "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", + "album_viewer_appbar_share_delete": "Delete album", + "album_viewer_appbar_share_err_delete": "Failed to delete album", + "album_viewer_appbar_share_err_leave": "Failed to leave album", + "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", + "album_viewer_appbar_share_err_title": "Failed to change album title", + "album_viewer_appbar_share_leave": "Leave album", + "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", + "album_viewer_page_share_add_users": "Add users", + "all": "All", + "all_people_page_title": "People", + "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", + "archived": "Archived", + "archive_page_no_archived_assets": "No archived assets found", + "archive_page_title": "Archive ({})", + "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", + "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_list_group_by_sub_title": "Group by", + "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", + "asset_list_layout_settings_group_automatically": "Automatic", + "asset_list_layout_settings_group_by": "Group assets by", + "asset_list_layout_settings_group_by_month": "Month", + "asset_list_layout_settings_group_by_month_day": "Month + day", + "asset_list_layout_sub_title": "Layout", + "asset_list_settings_subtitle": "Photo grid layout settings", + "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "asset_viewer_settings_title": "Asset Viewer", + "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", + "automatic_endpoint_switching_title": "Automatic URL switching", + "background_location_permission": "Background location permission", + "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "backup_album_selection_page_albums_device": "Albums on device ({})", + "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", + "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", + "backup_album_selection_page_select_albums": "Select albums", + "backup_album_selection_page_selection_info": "Selection Info", + "backup_album_selection_page_total_assets": "Total unique assets", + "backup_all": "All", + "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", + "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…", + "backup_background_service_current_upload_notification": "Uploading {}", + "backup_background_service_default_notification": "Checking for new assets…", + "backup_background_service_error_title": "Backup error", + "backup_background_service_in_progress_notification": "Backing up your assets…", + "backup_background_service_upload_failure_notification": "Failed to upload {}", + "backup_controller_page_albums": "Backup Albums", + "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", + "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", + "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", + "backup_controller_page_background_battery_info_link": "Show me how", + "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", + "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_battery_info_title": "Battery optimizations", + "backup_controller_page_background_charging": "Only while charging", + "backup_controller_page_background_configure_error": "Failed to configure the background service", + "backup_controller_page_background_delay": "Delay new assets backup: {}", + "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", + "backup_controller_page_background_is_off": "Automatic background backup is off", + "backup_controller_page_background_is_on": "Automatic background backup is on", + "backup_controller_page_background_turn_off": "Turn off background service", + "backup_controller_page_background_turn_on": "Turn on background service", + "backup_controller_page_background_wifi": "Only on WiFi", + "backup_controller_page_backup": "Backup", + "backup_controller_page_backup_selected": "Selected: ", + "backup_controller_page_backup_sub": "Backed up photos and videos", + "backup_controller_page_cancel": "Cancel", + "backup_controller_page_created": "Created on: {}", + "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", + "backup_controller_page_excluded": "Excluded: ", + "backup_controller_page_failed": "Failed ({})", + "backup_controller_page_filename": "File name: {} [{}]", + "backup_controller_page_id": "ID: {}", + "backup_controller_page_info": "Backup Information", + "backup_controller_page_none_selected": "None selected", + "backup_controller_page_remainder": "Remainder", + "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", + "backup_controller_page_select": "Select", + "backup_controller_page_server_storage": "Server Storage", + "backup_controller_page_start_backup": "Start Backup", + "backup_controller_page_status_off": "Automatic foreground backup is off", + "backup_controller_page_status_on": "Automatic foreground backup is on", + "backup_controller_page_storage_format": "{} of {} used", + "backup_controller_page_to_backup": "Albums to be backed up", + "backup_controller_page_total": "Total", + "backup_controller_page_total_sub": "All unique photos and videos from selected albums", + "backup_controller_page_turn_off": "Turn off foreground backup", + "backup_controller_page_turn_on": "Turn on foreground backup", + "backup_controller_page_uploading_file_info": "Uploading file info", + "backup_err_only_album": "Cannot remove the only album", + "backup_info_card_assets": "assets", + "backup_manual_cancelled": "Cancelled", + "backup_manual_failed": "Failed", + "backup_manual_in_progress": "Upload already in progress. Try after sometime", + "backup_manual_success": "Success", + "backup_manual_title": "Upload status", + "backup_options_page_title": "Backup options", + "backup_setting_subtitle": "Manage background and foreground upload settings", + "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", + "cache_settings_clear_cache_button": "Clear cache", + "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", + "cache_settings_duplicated_assets_clear_button": "CLEAR", + "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", + "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_image_cache_size": "Image cache size ({} assets)", + "cache_settings_statistics_album": "Library thumbnails", + "cache_settings_statistics_assets": "{} assets ({})", + "cache_settings_statistics_full": "Full images", + "cache_settings_statistics_shared": "Shared album thumbnails", + "cache_settings_statistics_thumbnail": "Thumbnails", + "cache_settings_statistics_title": "Cache usage", + "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", + "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", + "cache_settings_title": "Caching Settings", + "cancel": "Cancel", + "canceled": "Canceled", + "change_display_order": "Change display order", + "change_password_form_confirm_password": "Confirm Password", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_new_password": "New Password", + "change_password_form_password_mismatch": "Passwords do not match", + "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Check for corrupt asset backups", + "check_corrupt_asset_backup_button": "Perform check", + "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Enter Password", + "client_cert_import": "Import", + "client_cert_import_success_msg": "Client certificate is imported", + "client_cert_invalid_msg": "Invalid certificate file or wrong password", + "client_cert_remove": "Remove", + "client_cert_remove_msg": "Client certificate is removed", + "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", + "client_cert_title": "SSL Client Certificate (EXPERIMENTAL)", + "common_add_to_album": "Add to album", + "common_change_password": "Change Password", + "common_create_new_album": "Create new album", + "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", + "common_shared": "Shared", + "completed": "Completed", + "contextual_search": "Sunrise on the beach", + "control_bottom_app_bar_add_to_album": "Add to album", + "control_bottom_app_bar_album_info": "{} items", + "control_bottom_app_bar_album_info_shared": "{} items · Shared", + "control_bottom_app_bar_archive": "Archive", + "control_bottom_app_bar_create_new_album": "Create new album", + "control_bottom_app_bar_delete": "Delete", + "control_bottom_app_bar_delete_from_immich": "Delete from Immich", + "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_download": "Download", + "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit_location": "Edit Location", + "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_favorite": "Favorite", + "control_bottom_app_bar_share": "Share", + "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_unarchive": "Unarchive", + "control_bottom_app_bar_unfavorite": "Unfavorite", + "control_bottom_app_bar_upload": "Upload", + "create_album": "Create album", + "create_album_page_untitled": "Untitled", + "create_new": "CREATE NEW", + "create_shared_album_page_create": "Create", + "create_shared_album_page_share": "Share", + "create_shared_album_page_share_add_assets": "ADD ASSETS", + "create_shared_album_page_share_select_photos": "Select Photos", + "crop": "Crop", + "curated_location_page_title": "Places", + "curated_object_page_title": "Things", + "current_server_address": "Current server address", + "daily_title_text_date": "E, MMM dd", + "daily_title_text_date_year": "E, MMM dd, yyyy", + "date_format": "E, LLL d, y • h:mm a", + "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", + "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", + "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", + "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_cancel": "Cancel", + "delete_dialog_ok": "Delete", + "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_title": "Delete Permanently", + "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", + "delete_local_dialog_ok_force": "Delete Anyway", + "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", + "delete_shared_link_dialog_title": "Delete Shared Link", + "description_input_hint_text": "Add description...", + "description_input_submit_error": "Error updating description, check the log for more details", + "download_canceled": "Download canceled", + "download_complete": "Download complete", + "download_enqueue": "Download enqueued", + "download_error": "Download Error", + "download_failed": "Download failed", + "download_filename": "file: {}", + "download_finished": "Download finished", + "downloading": "Downloading...", + "downloading_media": "Downloading media", + "download_notfound": "Download not found", + "download_paused": "Download paused", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", + "download_waiting_to_retry": "Waiting to retry", + "edit_date_time_dialog_date_time": "Date and Time", + "edit_date_time_dialog_timezone": "Timezone", + "edit_image_title": "Edit", + "edit_location_dialog_title": "Location", + "end_date": "End date", + "enqueued": "Enqueued", + "enter_wifi_name": "Enter WiFi name", + "error_change_sort_album": "Failed to change album sort order", + "error_saving_image": "Error: {}", + "exif_bottom_sheet_description": "Add Description...", + "exif_bottom_sheet_details": "DETAILS", + "exif_bottom_sheet_location": "LOCATION", + "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", + "exif_bottom_sheet_person_add_person": "Add name", + "experimental_settings_new_asset_list_subtitle": "Work in progress", + "experimental_settings_new_asset_list_title": "Enable experimental photo grid", + "experimental_settings_subtitle": "Use at your own risk!", + "experimental_settings_title": "Experimental", + "external_network": "External network", + "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "failed": "Failed", + "favorites": "Favorites", + "favorites_page_no_favorites": "No favorite assets found", + "favorites_page_title": "Favorites", + "filename_search": "File name or extension", + "filter": "Filter", + "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "grant_permission": "Grant permission", + "haptic_feedback_switch": "Enable haptic feedback", + "haptic_feedback_title": "Haptic Feedback", + "header_settings_add_header_tip": "Add Header", + "header_settings_field_validator_msg": "Value cannot be empty", + "header_settings_header_name_input": "Header name", + "header_settings_header_value_input": "Header value", + "header_settings_page_title": "Proxy Headers (EXPERIMENTAL)", + "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", + "headers_settings_tile_title": "Custom proxy headers", + "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", + "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", + "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", + "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "ignore_icloud_photos": "Ignore iCloud photos", + "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", + "image_saved_successfully": "Image saved", + "image_viewer_page_state_provider_download_error": "Download Error", + "image_viewer_page_state_provider_download_started": "Download Started", + "image_viewer_page_state_provider_download_success": "Download Success", + "image_viewer_page_state_provider_share_error": "Share Error", + "invalid_date": "Invalid date", + "invalid_date_format": "Invalid date format", + "library": "Library", + "library_page_albums": "Albums", + "library_page_archive": "Archive", + "library_page_device_albums": "Albums on Device", + "library_page_favorites": "Favorites", + "library_page_new_album": "New album", + "library_page_sharing": "Sharing", + "library_page_sort_asset_count": "Number of assets", + "library_page_sort_created": "Created date", + "library_page_sort_last_modified": "Last modified", + "library_page_sort_most_oldest_photo": "Oldest photo", + "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_title": "Album title", + "local_network": "Local network", + "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", + "location_permission": "Location permission", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "location_picker_choose_on_map": "Choose on map", + "location_picker_latitude": "Latitude", + "location_picker_latitude_error": "Enter a valid latitude", + "location_picker_latitude_hint": "Enter your latitude here", + "location_picker_longitude": "Longitude", + "location_picker_longitude_error": "Enter a valid longitude", + "location_picker_longitude_hint": "Enter your longitude here", + "login_disabled": "Login has been disabled", + "login_form_api_exception": "API exception. Please check the server URL and try again.", + "login_form_back_button_text": "Back", + "login_form_button_text": "Login", + "login_form_email_hint": "youremail@email.com", + "login_form_endpoint_hint": "http://your-server-ip:port", + "login_form_endpoint_url": "Server Endpoint URL", + "login_form_err_http": "Please specify http:// or https://", + "login_form_err_invalid_email": "Invalid Email", + "login_form_err_invalid_url": "Invalid URL", + "login_form_err_leading_whitespace": "Leading whitespace", + "login_form_err_trailing_whitespace": "Trailing whitespace", + "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", + "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", + "login_form_failed_login": "Error logging you in, check server URL, email and password", + "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_label_email": "Email", + "login_form_label_password": "Password", + "login_form_next_button": "Next", + "login_form_password_hint": "password", + "login_form_save_login": "Stay logged in", + "login_form_server_empty": "Enter a server URL.", + "login_form_server_error": "Could not connect to server.", + "login_password_changed_error": "There was an error updating your password", + "login_password_changed_success": "Password updated successfully", + "map_assets_in_bound": "{} photo", + "map_assets_in_bounds": "{} photos", + "map_cannot_get_user_location": "Cannot get user's location", + "map_location_dialog_cancel": "Cancel", + "map_location_dialog_yes": "Yes", + "map_location_picker_page_use_location": "Use this location", + "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", + "map_location_service_disabled_title": "Location Service disabled", + "map_no_assets_in_bounds": "No photos in this area", + "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", + "map_no_location_permission_title": "Location Permission denied", + "map_settings_dark_mode": "Dark mode", + "map_settings_date_range_option_all": "All", + "map_settings_date_range_option_day": "Past 24 hours", + "map_settings_date_range_option_days": "Past {} days", + "map_settings_date_range_option_year": "Past year", + "map_settings_date_range_option_years": "Past {} years", + "map_settings_dialog_cancel": "Cancel", + "map_settings_dialog_save": "Save", + "map_settings_dialog_title": "Map Settings", + "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_partners": "Include Partners", + "map_settings_only_relative_range": "Date range", + "map_settings_only_show_favorites": "Show Favorite Only", + "map_settings_theme_settings": "Map Theme", + "map_zoom_to_see_photos": "Zoom out to see photos", + "memories_all_caught_up": "All caught up", + "memories_check_back_tomorrow": "Check back tomorrow for more memories", + "memories_start_over": "Start Over", + "memories_swipe_to_close": "Swipe up to close", + "memories_year_ago": "A year ago", + "memories_years_ago": "{} years ago", + "monthly_title_text_date_format": "MMMM y", + "motion_photos_page_title": "Motion Photos", + "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", + "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "my_albums": "My albums", + "networking_settings": "Networking", + "networking_subtitle": "Manage the server endpoint settings", + "no_assets_to_show": "No assets to show", + "no_name": "No name", + "notification_permission_dialog_cancel": "Cancel", + "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", + "notification_permission_dialog_settings": "Settings", + "notification_permission_list_tile_content": "Grant permission to enable notifications.", + "notification_permission_list_tile_enable_button": "Enable Notifications", + "notification_permission_list_tile_title": "Notification Permission", + "not_selected": "Not selected", + "on_this_device": "On this device", + "partner_list_user_photos": "{user}'s photos", + "partner_list_view_all": "View all", + "partner_page_add_partner": "Add partner", + "partner_page_empty_message": "Your photos are not yet shared with any partner.", + "partner_page_no_more_users": "No more users to add", + "partner_page_partner_add_failed": "Failed to add partner", + "partner_page_select_partner": "Select partner", + "partner_page_shared_to_title": "Shared to", + "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", + "partner_page_stop_sharing_title": "Stop sharing your photos?", + "partner_page_title": "Partner", + "partners": "Partners", + "paused": "Paused", + "people": "People", + "permission_onboarding_back": "Back", + "permission_onboarding_continue_anyway": "Continue anyway", + "permission_onboarding_get_started": "Get started", + "permission_onboarding_go_to_settings": "Go to settings", + "permission_onboarding_grant_permission": "Grant permission", + "permission_onboarding_log_out": "Log out", + "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", + "permission_onboarding_permission_granted": "Permission granted! You are all set.", + "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", + "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "places": "Places", + "preferences_settings_subtitle": "Manage the app's preferences", + "preferences_settings_title": "Preferences", + "profile_drawer_app_logs": "Logs", + "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", + "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", + "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_settings": "Settings", + "profile_drawer_sign_out": "Sign Out", + "profile_drawer_trash": "Trash", + "recently_added": "Recently added", + "recently_added_page_title": "Recently Added", + "save": "Save", + "save_to_gallery": "Save to gallery", + "scaffold_body_error_occurred": "Error occurred", + "search_albums": "Search albums", + "search_bar_hint": "Search your photos", + "search_filter_apply": "Apply filter", + "search_filter_camera": "Camera", + "search_filter_camera_make": "Make", + "search_filter_camera_model": "Model", + "search_filter_camera_title": "Select camera type", + "search_filter_date": "Date", + "search_filter_date_interval": "{start} to {end}", + "search_filter_date_title": "Select a date range", + "search_filter_display_option_archive": "Archive", + "search_filter_display_option_favorite": "Favorite", + "search_filter_display_option_not_in_album": "Not in album", + "search_filter_display_options": "Display Options", + "search_filter_display_options_title": "Display options", + "search_filter_location": "Location", + "search_filter_location_city": "City", + "search_filter_location_country": "Country", + "search_filter_location_state": "State", + "search_filter_location_title": "Select location", + "search_filter_media_type": "Media Type", + "search_filter_media_type_all": "All", + "search_filter_media_type_image": "Image", + "search_filter_media_type_title": "Select media type", + "search_filter_media_type_video": "Video", + "search_filter_people": "People", + "search_filter_people_title": "Select people", + "search_page_categories": "Categories", + "search_page_favorites": "Favorites", + "search_page_motion_photos": "Motion Photos", + "search_page_no_objects": "No Objects Info Available", + "search_page_no_places": "No Places Info Available", + "search_page_people": "People", + "search_page_person_add_name_dialog_cancel": "Cancel", + "search_page_person_add_name_dialog_hint": "Name", + "search_page_person_add_name_dialog_save": "Save", + "search_page_person_add_name_dialog_title": "Add a name", + "search_page_person_add_name_subtitle": "Find them fast by name with search", + "search_page_person_add_name_title": "Add a name", + "search_page_person_edit_name": "Edit name", + "search_page_places": "Places", + "search_page_recently_added": "Recently added", + "search_page_screenshots": "Screenshots", + "search_page_search_photos_videos": "Search for your photos and videos", + "search_page_selfies": "Selfies", + "search_page_things": "Things", + "search_page_videos": "Videos", + "search_page_view_all_button": "View all", + "search_page_your_activity": "Your activity", + "search_page_your_map": "Your Map", + "search_result_page_new_search_hint": "New Search", + "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", + "search_suggestion_list_smart_search_hint_2": "m:your-search-term", + "select_additional_user_for_sharing_page_suggestions": "Suggestions", + "select_user_for_sharing_page_err_album": "Failed to create album", + "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Server Endpoint", + "server_info_box_app_version": "App Version", + "server_info_box_latest_release": "Latest Version", + "server_info_box_server_url": "Server URL", + "server_info_box_server_version": "Server Version", + "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", + "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", + "setting_image_viewer_original_title": "Load original image", + "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", + "setting_image_viewer_preview_title": "Load preview image", + "setting_image_viewer_title": "Images", + "setting_languages_apply": "Apply", + "setting_languages_subtitle": "Change the app's language", + "setting_languages_title": "Languages", + "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", + "setting_notifications_notify_hours": "{} hours", + "setting_notifications_notify_immediately": "immediately", + "setting_notifications_notify_minutes": "{} minutes", + "setting_notifications_notify_never": "never", + "setting_notifications_notify_seconds": "{} seconds", + "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", + "setting_notifications_single_progress_title": "Show background backup detail progress", + "setting_notifications_subtitle": "Adjust your notification preferences", + "setting_notifications_title": "Notifications", + "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", + "setting_notifications_total_progress_title": "Show background backup total progress", + "setting_pages_app_bar_settings": "Settings", + "settings_require_restart": "Please restart Immich to apply this setting", + "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", + "setting_video_viewer_looping_title": "Looping", + "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", + "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_title": "Videos", + "share_add": "Add", + "share_add_photos": "Add photos", + "share_add_title": "Add a title", + "share_assets_selected": "{} selected", + "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", + "shared_album_section_people_action_error": "Error leaving/removing from album", + "shared_album_section_people_action_leave": "Remove user from album", + "shared_album_section_people_action_remove_user": "Remove user from album", + "shared_album_section_people_owner_label": "Owner", + "shared_album_section_people_title": "PEOPLE", + "share_dialog_preparing": "Preparing...", + "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_link_app_bar_title": "Shared Links", + "shared_link_clipboard_copied_massage": "Copied to clipboard", + "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_create_app_bar_title": "Create link to share", + "shared_link_create_error": "Error while creating shared link", + "shared_link_create_info": "Let anyone with the link see the selected photo(s)", + "shared_link_create_submit_button": "Create link", + "shared_link_edit_allow_download": "Allow public user to download", + "shared_link_edit_allow_upload": "Allow public user to upload", + "shared_link_edit_app_bar_title": "Edit link", + "shared_link_edit_change_expiry": "Change expiration time", + "shared_link_edit_description": "Description", + "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_expire_after_option_day": "1 day", + "shared_link_edit_expire_after_option_days": "{} days", + "shared_link_edit_expire_after_option_hour": "1 hour", + "shared_link_edit_expire_after_option_hours": "{} hours", + "shared_link_edit_expire_after_option_minute": "1 minute", + "shared_link_edit_expire_after_option_minutes": "{} minutes", + "shared_link_edit_expire_after_option_months": "{} months", + "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_year": "{} year", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", + "shared_link_edit_show_meta": "Show metadata", + "shared_link_edit_submit_button": "Update link", + "shared_link_empty": "You don't have any shared links", + "shared_link_error_server_url_fetch": "Cannot fetch the server url", + "shared_link_expired": "Expired", + "shared_link_expires_day": "Expires in {} day", + "shared_link_expires_days": "Expires in {} days", + "shared_link_expires_hour": "Expires in {} hour", + "shared_link_expires_hours": "Expires in {} hours", + "shared_link_expires_minute": "Expires in {} minute", + "shared_link_expires_minutes": "Expires in {} minutes", + "shared_link_expires_never": "Expires ∞", + "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_seconds": "Expires in {} seconds", + "shared_link_individual_shared": "Individual shared", + "shared_link_info_chip_download": "Download", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Upload", + "shared_link_manage_links": "Manage Shared links", + "shared_link_public_album": "Public album", + "shared_links": "Shared links", + "share_done": "Done", + "shared_with_me": "Shared with me", + "share_invite": "Invite to album", + "sharing_page_album": "Shared albums", + "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", + "sharing_page_empty_list": "EMPTY LIST", + "sharing_silver_appbar_create_shared_album": "New shared album", + "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_share_partner": "Share with partner", + "start_date": "Start date", + "sync": "Sync", + "sync_albums": "Sync albums", + "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", + "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "tab_controller_nav_library": "Library", + "tab_controller_nav_photos": "Photos", + "tab_controller_nav_search": "Search", + "tab_controller_nav_sharing": "Sharing", + "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", + "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_dark_mode_switch": "Dark mode", + "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", + "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", + "theme_setting_system_theme_switch": "Automatic (Follow system setting)", + "theme_setting_theme_subtitle": "Choose the app's theme setting", + "theme_setting_theme_title": "Theme", + "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", + "theme_setting_three_stage_loading_title": "Enable three-stage loading", + "translated_text_options": "Options", + "trash": "Trash", + "trash_emptied": "Emptied trash", + "trash_page_delete": "Delete", + "trash_page_delete_all": "Delete All", + "trash_page_empty_trash_btn": "Empty trash", + "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_info": "Trashed items will be permanently deleted after {} days", + "trash_page_no_assets": "No trashed assets", + "trash_page_restore": "Restore", + "trash_page_restore_all": "Restore All", + "trash_page_select_assets_btn": "Select assets", + "trash_page_select_btn": "Select", + "trash_page_title": "Trash ({})", + "upload": "Upload", + "upload_dialog_cancel": "Cancel", + "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", + "upload_dialog_ok": "Upload", + "upload_dialog_title": "Upload Asset", + "uploading": "Uploading", + "upload_to_immich": "Upload to Immich ({})", + "use_current_connection": "use current connection", + "validate_endpoint_error": "Please enter a valid URL", + "version_announcement_overlay_ack": "Acknowledge", + "version_announcement_overlay_release_notes": "release notes", + "version_announcement_overlay_text_1": "Hi friend, there is a new release of", + "version_announcement_overlay_text_2": "please take your time to visit the ", + "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", + "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "videos": "Videos", + "viewer_remove_from_stack": "Remove from Stack", + "viewer_stack_use_as_main_asset": "Use as Main Asset", + "viewer_unstack": "Un-Stack", + "wifi_name": "WiFi Name", + "your_wifi_name": "Your WiFi name" +} From ba01b40e7c3ff520c00463e7630d82b2ae517b16 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:01:55 -0500 Subject: [PATCH 140/184] fix(server): `sslmode` not working (#15587) * parse db url before passing it to the driver * don't be lazy * simplify * simplify * add tests * update sql sync script * update mock * remove unused import * remove unused imports --- server/src/app.module.ts | 16 +++- server/src/bin/sync-sql.ts | 4 +- .../repositories/config.repository.spec.ts | 78 +++++++++++++++++-- server/src/repositories/config.repository.ts | 75 +++++++++++------- server/src/services/database.service.spec.ts | 22 ++++-- .../repositories/config.repository.mock.ts | 9 +-- 6 files changed, 154 insertions(+), 50 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index cd1997220607c..0096cc6c26761 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -3,9 +3,11 @@ import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { PostgresJSDialect } from 'kysely-postgres-js'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; +import postgres from 'postgres'; import { commands } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; @@ -57,7 +59,19 @@ const imports = [ }, }), TypeOrmModule.forFeature(entities), - KyselyModule.forRoot(database.config.kysely), + KyselyModule.forRoot({ + dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }), + log(event) { + if (event.level === 'error') { + console.error('Query failed :', { + durationMs: event.queryDurationMillis, + error: event.error, + sql: event.query.sql, + params: event.query.parameters, + }); + } + }, + }), ]; class BaseModule implements OnModuleInit, OnModuleDestroy { diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index c25e1c8a90682..e0d578d58f923 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -4,10 +4,12 @@ import { Reflector } from '@nestjs/core'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { PostgresJSDialect } from 'kysely-postgres-js'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; +import postgres from 'postgres'; import { format } from 'sql-formatter'; import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators'; import { entities } from 'src/entities'; @@ -84,7 +86,7 @@ class SqlGenerator { const moduleFixture = await Test.createTestingModule({ imports: [ KyselyModule.forRoot({ - ...database.config.kysely, + dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }), log: (event) => { if (event.level === 'query') { this.sqlLogger.logQuery(event.query.sql); diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 19068ddc5d5bf..2b5343f7ba924 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -1,4 +1,3 @@ -import { PostgresJSDialect } from 'kysely-postgres-js'; import { ImmichTelemetry } from 'src/enum'; import { clearEnvCache, ConfigRepository } from 'src/repositories/config.repository'; @@ -81,10 +80,13 @@ describe('getEnv', () => { const { database } = getEnv(); expect(database).toEqual({ config: { - kysely: { - dialect: expect.any(PostgresJSDialect), - log: expect.any(Function), - }, + kysely: expect.objectContaining({ + host: 'database', + port: 5432, + database: 'immich', + username: 'postgres', + password: 'postgres', + }), typeorm: expect.objectContaining({ type: 'postgres', host: 'database', @@ -104,6 +106,72 @@ describe('getEnv', () => { const { database } = getEnv(); expect(database).toMatchObject({ skipMigrations: true }); }); + + it('should use DB_URL', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich'; + const { database } = getEnv(); + expect(database.config.kysely).toMatchObject({ + host: 'database1', + password: 'postgres2', + user: 'postgres1', + port: 54_320, + database: 'immich', + }); + }); + + it('should handle sslmode=require', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=prefer', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=verify-ca', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=verify-full', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=no-verify', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: { rejectUnauthorized: false } }); + }); + + it('should handle ssl=true', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ ssl: true }); + }); + + it('should reject invalid ssl', () => { + process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid'; + + expect(() => getEnv()).toThrowError('Invalid ssl option: invalid'); + }); }); describe('redis', () => { diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index d78e473da2281..a2af1b61b35c5 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -5,12 +5,11 @@ import { plainToInstance } from 'class-transformer'; import { validateSync } from 'class-validator'; import { Request, Response } from 'express'; import { RedisOptions } from 'ioredis'; -import { KyselyConfig } from 'kysely'; -import { PostgresJSDialect } from 'kysely-postgres-js'; import { CLS_ID, ClsModuleOptions } from 'nestjs-cls'; import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces'; import { join, resolve } from 'node:path'; -import postgres, { Notice } from 'postgres'; +import { parse } from 'pg-connection-string'; +import { Notice } from 'postgres'; import { citiesFile, excludePaths, IWorker } from 'src/constants'; import { Telemetry } from 'src/decorators'; import { EnvDto } from 'src/dtos/env.dto'; @@ -20,6 +19,20 @@ import { QueueName } from 'src/interfaces/job.interface'; import { setDifference } from 'src/utils/set'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; +type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; +type PostgresConnectionConfig = { + host?: string; + password?: string; + user?: string; + port?: number; + database?: string; + client_encoding?: string; + ssl?: Ssl; + application_name?: string; + fallback_application_name?: string; + options?: string; +}; + export interface EnvData { host?: string; port: number; @@ -53,7 +66,7 @@ export interface EnvData { }; database: { - config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig }; + config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: PostgresConnectionConfig }; skipMigrations: boolean; vectorExtension: VectorExtension; }; @@ -124,6 +137,9 @@ const asSet = (value: string | undefined, defaults: T[]) => { return new Set(values.length === 0 ? defaults : (values as T[])); }; +const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl => + typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full'; + const getEnv = (): EnvData => { const dto = plainToInstance(EnvDto, process.env); const errors = validateSync(dto); @@ -185,6 +201,31 @@ const getEnv = (): EnvData => { } } + const parts = { + connectionType: 'parts', + host: dto.DB_HOSTNAME || 'database', + port: dto.DB_PORT || 5432, + username: dto.DB_USERNAME || 'postgres', + password: dto.DB_PASSWORD || 'postgres', + database: dto.DB_DATABASE_NAME || 'immich', + } as const; + + let parsedOptions: PostgresConnectionConfig = parts; + if (dto.DB_URL) { + const parsed = parse(dto.DB_URL); + if (!isValidSsl(parsed.ssl)) { + throw new Error(`Invalid ssl option: ${parsed.ssl}`); + } + + parsedOptions = { + ...parsed, + ssl: parsed.ssl, + host: parsed.host ?? undefined, + port: parsed.port ? Number(parsed.port) : undefined, + database: parsed.database ?? undefined, + }; + } + const driverOptions = { onnotice: (notice: Notice) => { if (notice['severity'] !== 'NOTICE') { @@ -206,17 +247,9 @@ const getEnv = (): EnvData => { serialize: (value: number) => value.toString(), }, }, + ...parsedOptions, }; - const parts = { - connectionType: 'parts', - host: dto.DB_HOSTNAME || 'database', - port: dto.DB_PORT || 5432, - username: dto.DB_USERNAME || 'postgres', - password: dto.DB_PASSWORD || 'postgres', - database: dto.DB_DATABASE_NAME || 'immich', - } as const; - return { host: dto.IMMICH_HOST, port: dto.IMMICH_PORT || 2283, @@ -282,21 +315,7 @@ const getEnv = (): EnvData => { parseInt8: true, ...(databaseUrl ? { connectionType: 'url', url: databaseUrl } : parts), }, - kysely: { - dialect: new PostgresJSDialect({ - postgres: databaseUrl ? postgres(databaseUrl, driverOptions) : postgres({ ...parts, ...driverOptions }), - }), - log(event) { - if (event.level === 'error') { - console.error('Query failed :', { - durationMs: event.queryDurationMillis, - error: event.error, - sql: event.query.sql, - params: event.query.parameters, - }); - } - }, - }, + kysely: driverOptions, }, skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false, diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index 477cb6931f035..edd2f9dc626d9 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,4 +1,3 @@ -import { PostgresJSDialect } from 'kysely-postgres-js'; import { DatabaseExtension, EXTENSION_NAMES, @@ -62,8 +61,11 @@ describe(DatabaseService.name, () => { database: { config: { kysely: { - dialect: expect.any(PostgresJSDialect), - log: ['error'], + host: 'database', + port: 5432, + user: 'postgres', + password: 'postgres', + database: 'immich', }, typeorm: { connectionType: 'parts', @@ -298,8 +300,11 @@ describe(DatabaseService.name, () => { database: { config: { kysely: { - dialect: expect.any(PostgresJSDialect), - log: ['error'], + host: 'database', + port: 5432, + user: 'postgres', + password: 'postgres', + database: 'immich', }, typeorm: { connectionType: 'parts', @@ -328,8 +333,11 @@ describe(DatabaseService.name, () => { database: { config: { kysely: { - dialect: expect.any(PostgresJSDialect), - log: ['error'], + host: 'database', + port: 5432, + user: 'postgres', + password: 'postgres', + database: 'immich', }, typeorm: { connectionType: 'parts', diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index ab8731ea4d8b1..2b195ae8c9a62 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -1,5 +1,3 @@ -import { PostgresJSDialect } from 'kysely-postgres-js'; -import postgres from 'postgres'; import { ImmichEnvironment, ImmichWorker } from 'src/enum'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { EnvData } from 'src/repositories/config.repository'; @@ -24,12 +22,7 @@ const envData: EnvData = { database: { config: { - kysely: { - dialect: new PostgresJSDialect({ - postgres: postgres({ database: 'immich', host: 'database', port: 5432 }), - }), - log: ['error'], - }, + kysely: { database: 'immich', host: 'database', port: 5432 }, typeorm: { connectionType: 'parts', database: 'immich', From 9871a04d54288b464d54033ce6ca5d58a1137e6a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:09:06 +0000 Subject: [PATCH 141/184] chore: version v1.125.2 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index f7364aaa21334..75d01d7596fca 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.42", + "version": "2.2.43", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.42", + "version": "2.2.43", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index b995d852deadf..810e9c4b3ba50 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.42", + "version": "2.2.43", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 01975e79c1ab9..6a33867146c63 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.2", + "url": "https://v1.125.2.archive.immich.app" + }, { "label": "v1.125.1", "url": "https://v1.125.1.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 31b79ce44b22d..2e03f9f4b5cc7 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.1", + "version": "1.125.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.42", + "version": "2.2.43", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 73b4a2dc29045..35be7954f36f7 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.1", + "version": "1.125.2", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index a89916dc68e3e..15a23d1c95935 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.1" +version = "1.125.2" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index eec5f8bc88d52..be26d489dd0f0 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 177, - "android.injected.version.name" => "1.125.1", + "android.injected.version.code" => 178, + "android.injected.version.name" => "1.125.2", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index c88974a9e5842..9c2284a9f1199 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.1" + version_number: "1.125.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f239026c0a1e5..f14203b55d23b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.125.1 +- API version: 1.125.2 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 140ec7291dea6..d4b39e5cf010e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.1+177 +version: 1.125.2+178 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 7ce4e0e3003bb..5aac20c1c04e6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.1", + "version": "1.125.2", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 45410e78a09c5..d77e997d4b3c3 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 5d94e0e70de9f..e36eaa573395a 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7a4c0cd7ee631..ce521bf8472af 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.125.1 + * 1.125.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 248792918d9ac..18f066e7ebe10 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.1", + "version": "1.125.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 7f93d4e503f40..3ccd2db1797eb 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.1", + "version": "1.125.2", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 34366ca36848e..18d4413a06f42 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.1", + "version": "1.125.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 1402c0b86856e..7daaadf3ff7ef 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.1", + "version": "1.125.2", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 72fa31f9e9bf43af1cbedfa79a6dd5c2ed0563ad Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:01:24 -0500 Subject: [PATCH 142/184] fix(server): changing vector dim size (#15630) --- server/src/repositories/search.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index fb59157c8024f..a309f76e012ff 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -292,7 +292,7 @@ export class SearchRepository implements ISearchRepository { await sql`truncate ${sql.table('smart_search')}`.execute(trx); await trx.schema .alterTable('smart_search') - .alterColumn('embedding', (col) => col.setDataType(sql.lit(`vector(${dimSize})`))) + .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) .execute(); await sql`reindex index clip_index`.execute(trx); }); From 10e518db427ddcffae27832c43043678587fe3a7 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Sat, 25 Jan 2025 04:45:55 +0100 Subject: [PATCH 143/184] chore(server): print stack in case of worker error (#15632) feat: show error stack --- server/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main.ts b/server/src/main.ts index 3097eee69bd74..95b35c6915aea 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -13,7 +13,7 @@ if (immichApp) { let apiProcess: ChildProcess | undefined; const onError = (name: string, error: Error) => { - console.error(`${name} worker error: ${error}`); + console.error(`${name} worker error: ${error}, stack: ${error.stack}`); }; const onExit = (name: string, exitCode: number | null) => { From 39697cd9737980f41dc79f7b205a36f8fb112627 Mon Sep 17 00:00:00 2001 From: jdicioccio Date: Sat, 25 Jan 2025 00:26:52 -1000 Subject: [PATCH 144/184] fix: increase upload timeout (#15588) Fix upload timeout issue Fix an issue where when uploading a large file, the upload would consistently abort after 30 minutes. I changed this timeout from 30 minutes to 1 day. Maybe that's excessive, or maybe the timeout isn't even needed, but the current 30 minute timeout definitely seems way too short. --- server/src/workers/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index d6dc7233d1f2e..ddf6e50aa2e79 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -62,7 +62,7 @@ async function bootstrap() { app.use(app.get(ApiService).ssr(excludePaths)); const server = await (host ? app.listen(port, host) : app.listen(port)); - server.requestTimeout = 30 * 60 * 1000; + server.requestTimeout = 24 * 60 * 60 * 1000; logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${environment}] `); } From 79592701dd2abb2f913ab23aa023e49b9b4dcc87 Mon Sep 17 00:00:00 2001 From: Regenxyz <92148448+idkwhyiusethisname@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:30:53 +0700 Subject: [PATCH 145/184] chore: fix typos in Thai Language Readme (#15637) Update README_th_TH.md Fixing weird Thai Translate --- readme_i18n/README_th_TH.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/readme_i18n/README_th_TH.md b/readme_i18n/README_th_TH.md index 5a73251652070..7735c3c854829 100644 --- a/readme_i18n/README_th_TH.md +++ b/readme_i18n/README_th_TH.md @@ -40,12 +40,12 @@ Tiếng Việt

-## ข้อจำกัดความรับผิดชอบ +## ข้อควรระวัง -- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**ที่มีการเปลี่ยนแปลงบ่อยมาก** -- ⚠️ คาดว่าจะมีข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย -- ⚠️ **ห้ามใช้แอปนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ** -- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ! +- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**มีการเปลี่ยนแปลงบ่อยมาก** +- ⚠️ อาจจะเกิดข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย +- ⚠️ **ห้ามใช้ระบบนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ** +- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ > [!NOTE] > คุณสามารถหาคู่มือหลัก รวมถึงคู่มือการติดตั้ง ได้ที่ https://immich.app/ @@ -79,15 +79,15 @@ | :----------------------------------------- | ------ | ------ | | อัปโหลดและดูวิดีโอและภาพถ่าย | ใช่ | ใช่ | | การสำรองข้อมูลอัตโนมัติเมื่อเปิดแอป | ใช่ | N/A | -| ป้องกันการซ้ำซ้อนของไฟล์ | ใช่ | ใช่ | +| ป้องกันการซ้ำของไฟล์ | ใช่ | ใช่ | | เลือกอัลบั้มสำหรับสำรองข้อมูล | ใช่ | N/A | | ดาวน์โหลดภาพถ่ายและวิดีโอไปยังอุปกรณ์ | ใช่ | ใช่ | | รองรับผู้ใช้หลายคน | ใช่ | ใช่ | | อัลบั้มและอัลบั้มแชร์ | ใช่ | ใช่ | | แถบเลื่อนแบบลากได้ | ใช่ | ใช่ | | รองรับรูปแบบไฟล์ RAW | ใช่ | ใช่ | -| ดูข้อมูลเมตา (EXIF, แผนที่) | ใช่ | ใช่ | -| ค้นหาจากข้อมูลเมตา วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ | +| ดูข้อมูลเมตาดาต้า (EXIF, แผนที่) | ใช่ | ใช่ | +| ค้นหาจากข้อมูลเมตาดาต้า วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ | | ฟังก์ชันการจัดการผู้ดูแลระบบ | ไม่ใช่ | ใช่ | | การสำรองข้อมูลพื้นหลัง | ใช่ | N/A | | การเลื่อนแบบเสมือน | ใช่ | ใช่ | @@ -100,7 +100,7 @@ | การจัดเก็บและรายการโปรด | ใช่ | ใช่ | | แผนที่ทั่วโลก | ใช่ | ใช่ | | การแชร์กับคู่หู | ใช่ | ใช่ | -| การจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ | +| ระบบจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ | | ความทรงจำ (x ปีที่แล้ว) | ใช่ | ใช่ | | รองรับแบบออฟไลน์ | ใช่ | ไม่ใช่ | | แกลเลอรีแบบอ่านอย่างเดียว | ใช่ | ใช่ | @@ -108,13 +108,13 @@ ## การแปลภาษา -อ่านเพิ่มเติมเกี่ยวกับการแปลภาษา [ที่นี่](https://immich.app/docs/developer/translations) +อ่านเพิ่มเติมเกี่ยวกับการแปล [ที่นี่](https://immich.app/docs/developer/translations) สถานะการแปล -## กิจกรรมของคลังเก็บข้อมูล +## กิจกรรมของ Repository ![กิจกรรม](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "ภาพการวิเคราะห์ของ Repobeats") From 947c053c159cdfa6a520587b606740a7193d86fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Sat, 25 Jan 2025 02:38:00 -0800 Subject: [PATCH 146/184] chore(server): add DB_URL supports Unix sockets unit test (#15629) * test(server): DB_URL supports Unix sockets * chore: format --------- Co-authored-by: Alex Tran --- .../repositories/config.repository.spec.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 2b5343f7ba924..888d5c33ec0a9 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -172,6 +172,28 @@ describe('getEnv', () => { expect(() => getEnv()).toThrowError('Invalid ssl option: invalid'); }); + + it('should handle socket: URLs', () => { + process.env.DB_URL = 'socket:/run/postgresql?db=database1'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ + host: '/run/postgresql', + database: 'database1', + }); + }); + + it('should handle sockets in postgres: URLs', () => { + process.env.DB_URL = 'postgres:///database2?host=/path/to/socket'; + + const { database } = getEnv(); + + expect(database.config.kysely).toMatchObject({ + host: '/path/to/socket', + database: 'database2', + }); + }); }); describe('redis', () => { From d12b1c907d68a9f9e6e4d459f6367c05f5872f03 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 25 Jan 2025 11:58:07 -0600 Subject: [PATCH 147/184] fix(server): bulk update location (#15642) --- server/src/services/asset.service.spec.ts | 28 +++++++++++++++++++++++ server/src/services/asset.service.ts | 9 +++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index bf36c181fc602..8ff846d39d022 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -416,6 +416,34 @@ describe(AssetService.name, () => { await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); }); + + it('should not update Assets table if no relevant fields are provided', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + await sut.updateAll(authStub.admin, { + ids: ['asset-1'], + latitude: 0, + longitude: 0, + isArchived: undefined, + isFavorite: undefined, + duplicateId: undefined, + rating: undefined, + }); + expect(assetMock.updateAll).not.toHaveBeenCalled(); + }); + + it('should update Assets table if isArchived field is provided', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + await sut.updateAll(authStub.admin, { + ids: ['asset-1'], + latitude: 0, + longitude: 0, + isArchived: undefined, + isFavorite: false, + duplicateId: undefined, + rating: undefined, + }); + expect(assetMock.updateAll).toHaveBeenCalled(); + }); }); describe('deleteAll', () => { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 3913c0ce4cb44..99ddbb29cc9da 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -142,7 +142,14 @@ export class AssetService extends BaseService { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); } - await this.assetRepository.updateAll(ids, options); + if ( + options.isArchived != undefined || + options.isFavorite != undefined || + options.duplicateId != undefined || + options.rating != undefined + ) { + await this.assetRepository.updateAll(ids, options); + } } @OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK }) From 19f2f888eea7802ddb0c75f2c75ea65cf91106fc Mon Sep 17 00:00:00 2001 From: Gagan Yadav Date: Sun, 26 Jan 2025 01:06:49 +0530 Subject: [PATCH 148/184] fix(mobile): improve timezone picker (#15615) - Fix missing timezones - Remove the UTC prefix from timezone display text to align with web app - Remove unnecessary layout builder - Created a custom `DropdownSearchMenu` widget Co-authored-by: Alex --- mobile/assets/i18n/en-US.json | 1 + .../lib/widgets/common/date_time_picker.dart | 143 +++++++-------- .../widgets/common/dropdown_search_menu.dart | 169 ++++++++++++++++++ 3 files changed, 239 insertions(+), 74 deletions(-) create mode 100644 mobile/lib/widgets/common/dropdown_search_menu.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 9450b4b44f2b3..194871e18d351 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -248,6 +248,7 @@ "download_waiting_to_retry": "Waiting to retry", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", + "edit_date_time_dialog_search_timezone": "Search timezone...", "edit_image_title": "Edit", "edit_location_dialog_title": "Location", "end_date": "End date", diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index d90ee40e47368..4e4e24e18c96b 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart'; @@ -24,7 +25,7 @@ Future showDateTimePicker({ } String _getFormattedOffset(int offsetInMilli, tz.Location location) { - return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})"; + return "${location.name} (${Duration(milliseconds: offsetInMilli).formatAsOffset()})"; } class _DateTimePicker extends HookWidget { @@ -73,7 +74,6 @@ class _DateTimePicker extends HookWidget { // returns a list of location along with it's offset in duration List<_TimeZoneOffset> getAllTimeZones() { return tz.timeZoneDatabase.locations.values - .where((l) => !l.currentTimeZone.abbreviation.contains("0")) .map(_TimeZoneOffset.fromLocation) .sorted() .toList(); @@ -133,83 +133,78 @@ class _DateTimePicker extends HookWidget { context.pop(dtWithOffset); } - return LayoutBuilder( - builder: (context, constraint) => AlertDialog( - contentPadding: - const EdgeInsets.symmetric(vertical: 32, horizontal: 18), - actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text( - "action_common_cancel", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.colorScheme.error, + return AlertDialog( + contentPadding: const EdgeInsets.symmetric(vertical: 32, horizontal: 18), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + "action_common_cancel", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.colorScheme.error, + ), + ).tr(), + ), + TextButton( + onPressed: popWithDateTime, + child: Text( + "action_common_update", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "edit_date_time_dialog_date_time", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ).tr(), + const SizedBox(height: 32), + ListTile( + tileColor: context.colorScheme.surfaceContainerHighest, + shape: ShapeBorder.lerp( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), - ).tr(), - ), - TextButton( - onPressed: popWithDateTime, - child: Text( - "action_common_update", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), + 1, + ), + trailing: Icon( + Icons.edit_outlined, + size: 18, + color: context.primaryColor, + ), + title: Text( + DateFormat("dd-MM-yyyy hh:mm a").format(date.value), + style: context.textTheme.bodyMedium, ).tr(), + onTap: pickDate, ), - ], - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "edit_date_time_dialog_date_time", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ).tr(), - const SizedBox(height: 32), - ListTile( - tileColor: context.colorScheme.surfaceContainerHighest, - shape: ShapeBorder.lerp( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - 1, - ), - trailing: Icon( - Icons.edit_outlined, - size: 18, - color: context.primaryColor, - ), - title: Text( - DateFormat("dd-MM-yyyy hh:mm a").format(date.value), - style: context.textTheme.bodyMedium, - ).tr(), - onTap: pickDate, + const SizedBox(height: 24), + DropdownSearchMenu( + trailingIcon: Icon( + Icons.arrow_drop_down, + color: context.primaryColor, ), - const SizedBox(height: 24), - DropdownMenu( - width: 275, - menuHeight: 300, - trailingIcon: Icon( - Icons.arrow_drop_down, - color: context.primaryColor, - ), - hintText: "edit_date_time_dialog_timezone".tr(), - label: const Text('edit_date_time_dialog_timezone').tr(), - textStyle: context.textTheme.bodyMedium, - onSelected: (value) => tzOffset.value = value!, - initialSelection: tzOffset.value, - dropdownMenuEntries: menuEntries, - ), - ], - ), + hintText: "edit_date_time_dialog_timezone".tr(), + label: const Text('edit_date_time_dialog_timezone').tr(), + textStyle: context.textTheme.bodyMedium, + onSelected: (value) => tzOffset.value = value, + initialSelection: tzOffset.value, + dropdownMenuEntries: menuEntries, + ), + ], ), ); } diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart new file mode 100644 index 0000000000000..2fd5539b0103a --- /dev/null +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -0,0 +1,169 @@ +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class DropdownSearchMenu extends HookWidget { + const DropdownSearchMenu({ + super.key, + required this.dropdownMenuEntries, + this.initialSelection, + this.onSelected, + this.trailingIcon, + this.hintText, + this.label, + this.textStyle, + this.menuConstraints, + }); + + final List> dropdownMenuEntries; + final T? initialSelection; + final ValueChanged? onSelected; + final Widget? trailingIcon; + final String? hintText; + final Widget? label; + final TextStyle? textStyle; + final BoxConstraints? menuConstraints; + + @override + Widget build(BuildContext context) { + final selectedItem = useState?>( + dropdownMenuEntries + .firstWhereOrNull((item) => item.value == initialSelection), + ); + final showTimeZoneDropdown = useState(false); + + final effectiveConstraints = menuConstraints ?? + const BoxConstraints( + minWidth: 280, + maxWidth: 280, + minHeight: 0, + maxHeight: 280, + ); + + final inputDecoration = InputDecoration( + contentPadding: const EdgeInsets.fromLTRB(12, 4, 12, 4), + border: const OutlineInputBorder(), + suffixIcon: trailingIcon, + label: label, + hintText: hintText, + ).applyDefaults(context.themeData.inputDecorationTheme); + + if (!showTimeZoneDropdown.value) { + return ConstrainedBox( + constraints: effectiveConstraints, + child: GestureDetector( + onTap: () => showTimeZoneDropdown.value = true, + child: InputDecorator( + decoration: inputDecoration, + child: selectedItem.value != null + ? Text( + selectedItem.value!.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle, + ) + : null, + ), + ), + ); + } + + return ConstrainedBox( + constraints: effectiveConstraints, + child: Autocomplete>( + displayStringForOption: (option) => option.label, + optionsBuilder: (textEditingValue) { + return dropdownMenuEntries.where( + (item) => item.label + .toLowerCase() + .trim() + .contains(textEditingValue.text.toLowerCase().trim()), + ); + }, + onSelected: (option) { + selectedItem.value = option; + showTimeZoneDropdown.value = false; + onSelected?.call(option.value); + }, + fieldViewBuilder: (context, textEditingController, focusNode, _) { + return TextField( + autofocus: true, + focusNode: focusNode, + controller: textEditingController, + decoration: inputDecoration.copyWith( + hintText: "edit_date_time_dialog_search_timezone".tr(), + ), + maxLines: 1, + style: context.textTheme.bodyMedium, + expands: false, + onTapOutside: (event) { + showTimeZoneDropdown.value = false; + focusNode.unfocus(); + }, + onSubmitted: (_) { + showTimeZoneDropdown.value = false; + }, + ); + }, + optionsViewBuilder: (context, onSelected, options) { + // This widget is a copy of the default implementation. + // We have only changed the `constraints` parameter. + return Align( + alignment: Alignment.topLeft, + child: ConstrainedBox( + constraints: effectiveConstraints, + child: Material( + elevation: 4.0, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + return InkWell( + onTap: () => onSelected(option), + child: Builder( + builder: (BuildContext context) { + final bool highlight = + AutocompleteHighlightedOption.of(context) == + index; + if (highlight) { + SchedulerBinding.instance.addPostFrameCallback( + (Duration timeStamp) { + Scrollable.ensureVisible( + context, + alignment: 0.5, + ); + }, + debugLabel: 'AutocompleteOptions.ensureVisible', + ); + } + return Container( + color: highlight + ? Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.12) + : null, + padding: const EdgeInsets.all(16.0), + child: Text( + option.label, + style: textStyle, + ), + ); + }, + ), + ); + }, + ), + ), + ), + ); + }, + ), + ); + } +} From 64b92cb24c6eae5c92e125e459f995329c013916 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sat, 25 Jan 2025 20:50:37 +0100 Subject: [PATCH 149/184] fix(server): do not reset fileCreatedDate (#15650) When marking an offline asset as online again, do not reset the fileCreatedAt value. This value contains the "true" date, copied from exif.dateTimeOriginal. If we overwrite this value, we'd need to run the metadata extraction job again. Instead, we just leave the old (and correct) value in place. fixes #15640 --- server/src/services/library.service.spec.ts | 22 +++++++++++++++++++-- server/src/services/library.service.ts | 1 - 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 5f81d92ec2b3d..9f60e35dcced5 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -337,12 +337,31 @@ describe(LibraryService.name, () => { expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], { deletedAt: null, - fileCreatedAt: assetStub.trashedOffline.fileModifiedAt, fileModifiedAt: assetStub.trashedOffline.fileModifiedAt, isOffline: false, originalFileName: 'path.jpg', }); }); + + it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => { + const mockAssetJob: ILibraryAssetJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.trashedOffline); + storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); + + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.updateAll).toHaveBeenCalledWith( + [assetStub.trashedOffline.id], + expect.not.objectContaining({ + fileCreatedAt: expect.anything(), + }), + ); + }); }); it('should update file when mtime has changed', async () => { @@ -360,7 +379,6 @@ describe(LibraryService.name, () => { expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { fileModifiedAt: newMTime, - fileCreatedAt: newMTime, isOffline: false, originalFileName: 'photo.jpg', deletedAt: null, diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index dca1dec9e2786..daccf01dceedb 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -511,7 +511,6 @@ export class LibraryService extends BaseService { await this.assetRepository.updateAll([asset.id], { isOffline: false, deletedAt: null, - fileCreatedAt: mtime, fileModifiedAt: mtime, originalFileName: parse(asset.originalPath).base, }); From 4f725b95e1df57e17637e7a5e280c50f95fc6f6e Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sat, 25 Jan 2025 23:45:13 +0100 Subject: [PATCH 150/184] fix(server): do not count deleted assets for album summary (#15668) fixes #15645 fixes #15646 --- e2e/src/api/specs/album.e2e-spec.ts | 20 ++++++++++++++++++++ server/src/queries/album.repository.sql | 1 + server/src/repositories/album.repository.ts | 1 + 3 files changed, 22 insertions(+) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 5b40234e8dc47..1d142ac468dbe 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -362,6 +362,26 @@ describe('/albums', () => { shared: true, }); }); + + it('should not count trashed assets', async () => { + await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); + + const { status, body } = await request(app) + .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user2Albums[0], + assets: [], + assetCount: 1, + lastModifiedAssetTimestamp: expect.any(String), + endDate: expect.any(String), + startDate: expect.any(String), + albumUsers: expect.any(Array), + shared: true, + }); + }); }); describe('GET /albums/statistics', () => { diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 48dc4dda4e5aa..b982ea2cffd96 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -210,6 +210,7 @@ from left join "assets" on "assets"."id" = "album_assets"."assetsId" where "albums"."id" in ($1) + and "assets"."deletedAt" is null group by "albums"."id" diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index d3b696169b748..c6e01b532e535 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -126,6 +126,7 @@ export class AlbumRepository implements IAlbumRepository { .select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate')) .select((eb) => eb.fn.count('assets.id').as('assetCount')) .where('albums.id', 'in', ids) + .where('assets.deletedAt', 'is', null) .groupBy('albums.id') .execute(); From 05a446c259a3c1658934c85abdad9574fbdb2cf3 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 25 Jan 2025 23:37:19 -0500 Subject: [PATCH 151/184] fix(server): avoid duplicate rows in album queries (#15670) * avoid duplicate rows * left join, handle null vs. undefined * update sql --- e2e/src/api/specs/album.e2e-spec.ts | 5 +- server/src/interfaces/album.interface.ts | 4 +- server/src/queries/album.repository.sql | 131 ++++++++------------ server/src/repositories/album.repository.ts | 69 ++++++----- server/src/services/album.service.spec.ts | 8 +- server/src/services/album.service.ts | 16 +-- 6 files changed, 103 insertions(+), 130 deletions(-) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 1d142ac468dbe..5c087d126977b 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -52,7 +52,10 @@ describe('/albums', () => { user1Albums = await Promise.all([ utils.createAlbum(user1.accessToken, { albumName: user1SharedEditorUser, - albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }], + albumUsers: [ + { userId: admin.userId, role: AlbumUserRole.Editor }, + { userId: user2.userId, role: AlbumUserRole.Editor }, + ], assetIds: [user1Asset1.id], }), utils.createAlbum(user1.accessToken, { diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts index 7af1bd97e10bc..36a6d8a1d2676 100644 --- a/server/src/interfaces/album.interface.ts +++ b/server/src/interfaces/album.interface.ts @@ -9,8 +9,8 @@ export const IAlbumRepository = 'IAlbumRepository'; export interface AlbumAssetCount { albumId: string; assetCount: number; - startDate: Date | undefined; - endDate: Date | undefined; + startDate: Date | null; + endDate: Date | null; } export interface AlbumInfoOptions { diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index b982ea2cffd96..217b0ce77bf40 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -90,7 +90,7 @@ select ( select "assets".*, - to_json("exif") as "exifInfo" + "exif" as "exifInfo" from "assets" inner join "exif" on "assets"."id" = "exif"."assetId" @@ -180,19 +180,20 @@ select ) as "albumUsers" from "albums" - left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" - left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "albums"."id" + inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" where ( - ( - "albums"."ownerId" = $1 - and "album_assets"."assetsId" = $2 - ) - or ( - "album_users"."usersId" = $3 - and "album_assets"."assetsId" = $4 + "albums"."ownerId" = $1 + or exists ( + select + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + and "album_users"."usersId" = $2 ) ) + and "album_assets"."assetsId" = $3 and "albums"."deletedAt" is null order by "albums"."createdAt" desc, @@ -200,10 +201,10 @@ order by -- AlbumRepository.getMetadataForIds select - "albums"."id", + "albums"."id" as "albumId", min("assets"."fileCreatedAt") as "startDate", max("assets"."fileCreatedAt") as "endDate", - count("assets"."id") as "assetCount" + count("assets"."id")::int as "assetCount" from "albums" left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" @@ -306,8 +307,8 @@ order by "albums"."createdAt" desc -- AlbumRepository.getShared -select distinct - on ("albums"."createdAt") "albums".*, +select + "albums".*, ( select coalesce(json_agg(agg), '[]') @@ -390,15 +391,26 @@ select distinct ) as "sharedLinks" from "albums" - left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id" - left join "shared_links" on "shared_links"."albumId" = "albums"."id" where ( - "shared_albums"."usersId" = $1 - or "shared_links"."userId" = $2 - or ( - "albums"."ownerId" = $3 - and "shared_albums"."usersId" is not null + exists ( + select + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + and ( + "albums"."ownerId" = $1 + or "album_users"."usersId" = $2 + ) + ) + or exists ( + select + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + and "shared_links"."userId" = $3 ) ) and "albums"."deletedAt" is null @@ -406,48 +418,8 @@ order by "albums"."createdAt" desc -- AlbumRepository.getNotShared -select distinct - on ("albums"."createdAt") "albums".*, - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "album_users".*, - ( - select - to_json(obj) - from - ( - select - "id", - "email", - "createdAt", - "profileImagePath", - "isAdmin", - "shouldChangePassword", - "deletedAt", - "oauthId", - "updatedAt", - "storageLabel", - "name", - "quotaSizeInBytes", - "quotaUsageInBytes", - "status", - "profileChangedAt" - from - "users" - where - "users"."id" = "album_users"."usersId" - ) as obj - ) as "user" - from - "albums_shared_users_users" as "album_users" - where - "album_users"."albumsId" = "albums"."id" - ) as agg - ) as "albumUsers", +select + "albums".*, ( select to_json(obj) @@ -474,29 +446,26 @@ select distinct where "users"."id" = "albums"."ownerId" ) as obj - ) as "owner", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - * - from - "shared_links" - where - "shared_links"."albumId" = "albums"."id" - ) as agg - ) as "sharedLinks" + ) as "owner" from "albums" - left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id" - left join "shared_links" on "shared_links"."albumId" = "albums"."id" where "albums"."ownerId" = $1 - and "shared_albums"."usersId" is null - and "shared_links"."userId" is null and "albums"."deletedAt" is null + and not exists ( + select + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) + and not exists ( + select + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + ) order by "albums"."createdAt" desc diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index c6e01b532e535..d63fd2ed4f2d5 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -59,7 +59,7 @@ const withAssets = (eb: ExpressionBuilder) => { .selectFrom('assets') .selectAll('assets') .innerJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.fn.toJson('exif').as('exifInfo')) + .select((eb) => eb.table('exif').as('exifInfo')) .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) @@ -93,14 +93,19 @@ export class AlbumRepository implements IAlbumRepository { return this.db .selectFrom('albums') .selectAll('albums') - .leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') - .leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'albums.id') + .innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') .where((eb) => eb.or([ - eb.and([eb('albums.ownerId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]), - eb.and([eb('album_users.usersId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]), + eb('albums.ownerId', '=', ownerId), + eb.exists( + eb + .selectFrom('albums_shared_users_users as album_users') + .whereRef('album_users.albumsId', '=', 'albums.id') + .where('album_users.usersId', '=', ownerId), + ), ]), ) + .where('album_assets.assetsId', '=', assetId) .where('albums.deletedAt', 'is', null) .orderBy('albums.createdAt', 'desc') .select(withOwner) @@ -117,25 +122,18 @@ export class AlbumRepository implements IAlbumRepository { return []; } - const metadatas = await this.db + return this.db .selectFrom('albums') .leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') .leftJoin('assets', 'assets.id', 'album_assets.assetsId') - .select('albums.id') + .select('albums.id as albumId') .select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate')) .select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate')) - .select((eb) => eb.fn.count('assets.id').as('assetCount')) + .select((eb) => sql`${eb.fn.count('assets.id')}::int`.as('assetCount')) .where('albums.id', 'in', ids) .where('assets.deletedAt', 'is', null) .groupBy('albums.id') .execute(); - - return metadatas.map((metadatas) => ({ - albumId: metadatas.id, - assetCount: Number(metadatas.assetCount), - startDate: metadatas.startDate ? new Date(metadatas.startDate) : undefined, - endDate: metadatas.endDate ? new Date(metadatas.endDate) : undefined, - })); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -160,14 +158,20 @@ export class AlbumRepository implements IAlbumRepository { return this.db .selectFrom('albums') .selectAll('albums') - .distinctOn('albums.createdAt') - .leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id') - .leftJoin('shared_links', 'shared_links.albumId', 'albums.id') .where((eb) => eb.or([ - eb('shared_albums.usersId', '=', ownerId), - eb('shared_links.userId', '=', ownerId), - eb.and([eb('albums.ownerId', '=', ownerId), eb('shared_albums.usersId', 'is not', null)]), + eb.exists( + eb + .selectFrom('albums_shared_users_users as album_users') + .whereRef('album_users.albumsId', '=', 'albums.id') + .where((eb) => eb.or([eb('albums.ownerId', '=', ownerId), eb('album_users.usersId', '=', ownerId)])), + ), + eb.exists( + eb + .selectFrom('shared_links') + .whereRef('shared_links.albumId', '=', 'albums.id') + .where('shared_links.userId', '=', ownerId), + ), ]), ) .where('albums.deletedAt', 'is', null) @@ -186,16 +190,21 @@ export class AlbumRepository implements IAlbumRepository { return this.db .selectFrom('albums') .selectAll('albums') - .distinctOn('albums.createdAt') - .leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id') - .leftJoin('shared_links', 'shared_links.albumId', 'albums.id') .where('albums.ownerId', '=', ownerId) - .where('shared_albums.usersId', 'is', null) - .where('shared_links.userId', 'is', null) .where('albums.deletedAt', 'is', null) - .select(withAlbumUsers) + .where((eb) => + eb.not( + eb.exists( + eb + .selectFrom('albums_shared_users_users as album_users') + .whereRef('album_users.albumsId', '=', 'albums.id'), + ), + ), + ) + .where((eb) => + eb.not(eb.exists(eb.selectFrom('shared_links').whereRef('shared_links.albumId', '=', 'albums.id'))), + ) .select(withOwner) - .select(withSharedLink) .orderBy('albums.createdAt', 'desc') .execute() as unknown as Promise; } @@ -282,7 +291,6 @@ export class AlbumRepository implements IAlbumRepository { .selectAll() .where('id', '=', newAlbum.id) .select(withOwner) - .select(withSharedLink) .select(withAssets) .select(withAlbumUsers) .executeTakeFirst() as unknown as Promise; @@ -292,7 +300,7 @@ export class AlbumRepository implements IAlbumRepository { update(id: string, album: Updateable): Promise { return this.db .updateTable('albums') - .set({ ...album, updatedAt: new Date() }) + .set(album) .where('id', '=', id) .returningAll('albums') .returning(withOwner) @@ -335,7 +343,6 @@ export class AlbumRepository implements IAlbumRepository { .select('album_assets.assetsId') .orderBy('assets.fileCreatedAt', 'desc') .limit(1), - updatedAt: new Date(), })) .where((eb) => eb.or([ diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index fe732843b6eb0..942615b0d9efc 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -52,8 +52,8 @@ describe(AlbumService.name, () => { it('gets list of albums for auth user', async () => { albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); albumMock.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, - { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, + { albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null }, + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null }, ]); const result = await sut.getAll(authStub.admin, {}); @@ -82,7 +82,7 @@ describe(AlbumService.name, () => { it('gets list of albums that are shared', async () => { albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]); albumMock.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null }, ]); const result = await sut.getAll(authStub.admin, { shared: true }); @@ -94,7 +94,7 @@ describe(AlbumService.name, () => { it('gets list of albums that are NOT shared', async () => { albumMock.getNotShared.mockResolvedValue([albumStub.empty]); albumMock.getMetadataForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, + { albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null }, ]); const result = await sut.getAll(authStub.admin, { shared: false }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index efc71c4c8d9a6..0b6c646801aee 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -55,13 +55,7 @@ export class AlbumService extends BaseService { const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); const albumMetadata: Record = {}; for (const metadata of results) { - const { albumId, assetCount, startDate, endDate } = metadata; - albumMetadata[albumId] = { - albumId, - assetCount, - startDate, - endDate, - }; + albumMetadata[metadata.albumId] = metadata; } return Promise.all( @@ -70,8 +64,8 @@ export class AlbumService extends BaseService { return { ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - startDate: albumMetadata[album.id].startDate, - endDate: albumMetadata[album.id].endDate, + startDate: albumMetadata[album.id].startDate ?? undefined, + endDate: albumMetadata[album.id].endDate ?? undefined, assetCount: albumMetadata[album.id].assetCount, lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, }; @@ -89,8 +83,8 @@ export class AlbumService extends BaseService { return { ...mapAlbum(album, withAssets, auth), - startDate: albumMetadataForIds.startDate, - endDate: albumMetadataForIds.endDate, + startDate: albumMetadataForIds.startDate ?? undefined, + endDate: albumMetadataForIds.endDate ?? undefined, assetCount: albumMetadataForIds.assetCount, lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, }; From 7bbffccf7601f0159629eb90a53db0e199a578df Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 26 Jan 2025 07:06:26 -0600 Subject: [PATCH 152/184] fix(web): neon overflow on mobile screen (#15676) --- web/src/lib/components/layouts/AuthPageLayout.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte index 19108956ff3f7..7eae5d08472a2 100644 --- a/web/src/lib/components/layouts/AuthPageLayout.svelte +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -11,7 +11,11 @@
- Immich logo + Immich logo
From f780a56e24b6d15ed14366f5ec942eff42c5babb Mon Sep 17 00:00:00 2001 From: Damiano Ferrari <34270884+ferraridamiano@users.noreply.github.com> Date: Sun, 26 Jan 2025 14:51:46 +0100 Subject: [PATCH 153/184] fix(mobile): Misaligned text icon in circle avatar (#15683) style(mobile): Use `DefaultTextStyle` for the text icon in `CircleAvatar` --- mobile/lib/widgets/common/user_circle_avatar.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 50da0096764ad..f90da6097b6cc 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -28,8 +28,7 @@ class UserCircleAvatar extends ConsumerWidget { final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; - final textIcon = Text( - user.name[0].toUpperCase(), + final textIcon = DefaultTextStyle( style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, @@ -37,6 +36,7 @@ class UserCircleAvatar extends ConsumerWidget { ? Colors.black : Colors.white, ), + child: Text(user.name[0].toUpperCase()), ); return CircleAvatar( backgroundColor: user.avatarColor.toColor(), From 206412267ac4abbd0617e11dd74fc843f1c4a026 Mon Sep 17 00:00:00 2001 From: sudbrack Date: Sun, 26 Jan 2025 08:06:18 -0600 Subject: [PATCH 154/184] fix(server): /search/random API returns same assets every call (#15682) * Fix for server searchRandom function not returning random results * Fix lint --- server/src/queries/search.repository.sql | 4 ++-- server/src/repositories/search.repository.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 2d5da4d381112..72e8a6941d362 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -36,7 +36,7 @@ offset and "assets"."deletedAt" is null and "assets"."id" < $6 order by - "assets"."id" + random() limit $7 ) @@ -56,7 +56,7 @@ union all and "assets"."deletedAt" is null and "assets"."id" > $13 order by - "assets"."id" + random() limit $14 ) diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a309f76e012ff..76b6653e3d73e 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -72,8 +72,14 @@ export class SearchRepository implements ISearchRepository { async searchRandom(size: number, options: AssetSearchOptions): Promise { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); - const lessThan = builder.where('assets.id', '<', uuid).orderBy('assets.id').limit(size); - const greaterThan = builder.where('assets.id', '>', uuid).orderBy('assets.id').limit(size); + const lessThan = builder + .where('assets.id', '<', uuid) + .orderBy(sql`random()`) + .limit(size); + const greaterThan = builder + .where('assets.id', '>', uuid) + .orderBy(sql`random()`) + .limit(size); const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); return rows as any as AssetEntity[]; } From 72a55c13b60036adccd9ea108276ca9a4bc37ade Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 14:14:48 +0000 Subject: [PATCH 155/184] chore: version v1.125.3 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 75d01d7596fca..6fcbbcc5c957a 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.43", + "version": "2.2.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.43", + "version": "2.2.44", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 810e9c4b3ba50..e90f48a2e21a4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.43", + "version": "2.2.44", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 6a33867146c63..422ce6a8fbe39 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.3", + "url": "https://v1.125.3.archive.immich.app" + }, { "label": "v1.125.2", "url": "https://v1.125.2.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 2e03f9f4b5cc7..726dc58adc6d8 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.2", + "version": "1.125.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.43", + "version": "2.2.44", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 35be7954f36f7..ad19e4d0599d3 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.2", + "version": "1.125.3", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 15a23d1c95935..9c0600bc1f057 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.2" +version = "1.125.3" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index be26d489dd0f0..824fdd9a97378 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 178, - "android.injected.version.name" => "1.125.2", + "android.injected.version.code" => 179, + "android.injected.version.name" => "1.125.3", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 9c2284a9f1199..4c86002f5c410 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.2" + version_number: "1.125.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f14203b55d23b..e0a88fcb43b4a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.125.2 +- API version: 1.125.3 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index d4b39e5cf010e..29bbc862473e3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.2+178 +version: 1.125.3+179 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5aac20c1c04e6..815dc7452dc34 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.2", + "version": "1.125.3", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index d77e997d4b3c3..3ec67cee5fbc9 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index e36eaa573395a..7e1b6d66991ed 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ce521bf8472af..3cfa15268fa1a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.125.2 + * 1.125.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 18f066e7ebe10..10c21a8c04392 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.2", + "version": "1.125.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 3ccd2db1797eb..5356b4c11b135 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.2", + "version": "1.125.3", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 18d4413a06f42..491ac70e853e8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.2", + "version": "1.125.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 7daaadf3ff7ef..861301ea0abdc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.2", + "version": "1.125.3", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From e864811a85aa27893ae2c3536d402509ccf51ca6 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 26 Jan 2025 22:07:22 +0100 Subject: [PATCH 156/184] fix(web): sort folders (#15691) fixes #13145 --- web/src/lib/components/shared-components/tree/tree-items.svelte | 2 +- web/src/lib/stores/folders.svelte.ts | 1 - .../folders/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- .../(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte index c6db9fec8d684..3724ced6c9e4d 100644 --- a/web/src/lib/components/shared-components/tree/tree-items.svelte +++ b/web/src/lib/components/shared-components/tree/tree-items.svelte @@ -15,7 +15,7 @@
    - {#each Object.entries(items) as [path, tree]} + {#each Object.entries(items).sort() as [path, tree]} {@const value = normalizeTreePath(`${parent}/${path}`)} {@const key = value + getColor(value)} {#key key} diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts index f3b237f8a2ed1..fb59687a38121 100644 --- a/web/src/lib/stores/folders.svelte.ts +++ b/web/src/lib/stores/folders.svelte.ts @@ -24,7 +24,6 @@ class FoldersStore { const uniquePaths = await getUniqueOriginalPaths(); this.uniquePaths.push(...uniquePaths); - this.uniquePaths.sort(); } bustAssetCache() { diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 87cd2434d61ff..8ff2a35981b27 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -44,7 +44,7 @@ let pathSegments = $derived(data.path ? data.path.split('/') : []); let tree = $derived(buildTree(foldersStore.uniquePaths)); let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); - let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); + let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort()); const assetInteraction = new AssetInteraction(); diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts index 0d23ba32df790..d00ba238ef869 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -34,7 +34,7 @@ export const load = (async ({ params, url }) => { return { asset, path, - currentFolders: Object.keys(tree || {}), + currentFolders: Object.keys(tree || {}).sort(), pathAssets, meta: { title: $t('folders'), From 8dab5d37980a115fbc14dbf3486c18a07722883d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 26 Jan 2025 15:09:15 -0600 Subject: [PATCH 157/184] chore(mobile): post release task (#15662) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index d7d24a9fa91ed..89c18ba5b75aa 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +748,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -791,7 +791,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -831,7 +831,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 189; + CURRENT_PROJECT_VERSION = 190; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index a3b34a9bcdbaa..a2688775dc042 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.125.1 + 1.125.2 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 189 + 190 FLTEnableImpeller ITSAppUsesNonExemptEncryption From f6cbc9db06c0783d09f154f66e12d041032fff62 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 26 Jan 2025 21:18:34 -0600 Subject: [PATCH 158/184] fix(server): cannot render album page when all assets of an album are in trash (#15690) * fix(server): cannot render album page when all assets of an album are in trash * inner join * add e2e test * check empty albums too * render add to album button on empty album * lint * count 0 if undefined * fix album card test --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> --- e2e/src/api/specs/album.e2e-spec.ts | 129 ++++++++++++-------- server/src/queries/album.repository.sql | 4 +- server/src/repositories/album.repository.ts | 4 +- server/src/services/album.service.ts | 12 +- 4 files changed, 91 insertions(+), 58 deletions(-) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 5c087d126977b..cede49f4690f4 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -22,82 +22,92 @@ const user1NotShared = 'user1NotShared'; const user2SharedUser = 'user2SharedUser'; const user2SharedLink = 'user2SharedLink'; const user2NotShared = 'user2NotShared'; +const user4DeletedAsset = 'user4DeletedAsset'; +const user4Empty = 'user4Empty'; describe('/albums', () => { let admin: LoginResponseDto; let user1: LoginResponseDto; let user1Asset1: AssetMediaResponseDto; let user1Asset2: AssetMediaResponseDto; + let user4Asset1: AssetMediaResponseDto; let user1Albums: AlbumResponseDto[]; let user2: LoginResponseDto; let user2Albums: AlbumResponseDto[]; + let deletedAssetAlbum: AlbumResponseDto; let user3: LoginResponseDto; // deleted + let user4: LoginResponseDto; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup(); - [user1, user2, user3] = await Promise.all([ + [user1, user2, user3, user4] = await Promise.all([ utils.userSetup(admin.accessToken, createUserDto.user1), utils.userSetup(admin.accessToken, createUserDto.user2), utils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user4), ]); - [user1Asset1, user1Asset2] = await Promise.all([ + [user1Asset1, user1Asset2, user4Asset1] = await Promise.all([ utils.createAsset(user1.accessToken, { isFavorite: true }), utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), ]); - user1Albums = await Promise.all([ - utils.createAlbum(user1.accessToken, { - albumName: user1SharedEditorUser, - albumUsers: [ - { userId: admin.userId, role: AlbumUserRole.Editor }, - { userId: user2.userId, role: AlbumUserRole.Editor }, - ], - assetIds: [user1Asset1.id], - }), - utils.createAlbum(user1.accessToken, { - albumName: user1SharedLink, - assetIds: [user1Asset1.id], - }), - utils.createAlbum(user1.accessToken, { - albumName: user1NotShared, - assetIds: [user1Asset1.id, user1Asset2.id], - }), - utils.createAlbum(user1.accessToken, { - albumName: user1SharedViewerUser, - albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], - assetIds: [user1Asset1.id], - }), - ]); - - user2Albums = await Promise.all([ - utils.createAlbum(user2.accessToken, { - albumName: user2SharedUser, - albumUsers: [ - { userId: user1.userId, role: AlbumUserRole.Editor }, - { userId: user3.userId, role: AlbumUserRole.Editor }, - ], + [user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([ + Promise.all([ + utils.createAlbum(user1.accessToken, { + albumName: user1SharedEditorUser, + albumUsers: [ + { userId: admin.userId, role: AlbumUserRole.Editor }, + { userId: user2.userId, role: AlbumUserRole.Editor }, + ], + assetIds: [user1Asset1.id], + }), + utils.createAlbum(user1.accessToken, { + albumName: user1SharedLink, + assetIds: [user1Asset1.id], + }), + utils.createAlbum(user1.accessToken, { + albumName: user1NotShared, + assetIds: [user1Asset1.id, user1Asset2.id], + }), + utils.createAlbum(user1.accessToken, { + albumName: user1SharedViewerUser, + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], + assetIds: [user1Asset1.id], + }), + ]), + Promise.all([ + utils.createAlbum(user2.accessToken, { + albumName: user2SharedUser, + albumUsers: [ + { userId: user1.userId, role: AlbumUserRole.Editor }, + { userId: user3.userId, role: AlbumUserRole.Editor }, + ], + }), + utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + ]), + utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }), + utils.createAlbum(user4.accessToken, { albumName: user4Empty }), + utils.createAlbum(user3.accessToken, { + albumName: 'Deleted', + albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }], }), - utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), - utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), ]); - await utils.createAlbum(user3.accessToken, { - albumName: 'Deleted', - albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }], - }); - - await addAssetsToAlbum( - { id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } }, - { headers: asBearerAuth(user1.accessToken) }, - ); - - user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }); - await Promise.all([ + addAssetsToAlbum( + { id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } }, + { headers: asBearerAuth(user1.accessToken) }, + ), + addAssetsToAlbum( + { id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } }, + { headers: asBearerAuth(user4.accessToken) }, + ), // add shared link to user1SharedLink album utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, @@ -110,7 +120,11 @@ describe('/albums', () => { }), ]); - await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); + [user2Albums[0]] = await Promise.all([ + getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }), + deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }), + utils.deleteAssets(user1.accessToken, [user4Asset1.id]), + ]); }); describe('GET /albums', () => { @@ -287,6 +301,25 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toHaveLength(5); }); + + it('should return empty albums and albums where all assets are deleted', async () => { + const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ownerId: user4.userId, + albumName: user4DeletedAsset, + shared: false, + }), + expect.objectContaining({ + ownerId: user4.userId, + albumName: user4Empty, + shared: false, + }), + ]), + ); + }); }); describe('GET /albums/:id', () => { diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 217b0ce77bf40..08ea078f730cd 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -207,8 +207,8 @@ select count("assets"."id")::int as "assetCount" from "albums" - left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" - left join "assets" on "assets"."id" = "album_assets"."assetsId" + inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" + inner join "assets" on "assets"."id" = "album_assets"."assetsId" where "albums"."id" in ($1) and "assets"."deletedAt" is null diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index d63fd2ed4f2d5..6c81395a58300 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -124,8 +124,8 @@ export class AlbumRepository implements IAlbumRepository { return this.db .selectFrom('albums') - .leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') - .leftJoin('assets', 'assets.id', 'album_assets.assetsId') + .innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') + .innerJoin('assets', 'assets.id', 'album_assets.assetsId') .select('albums.id as albumId') .select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate')) .select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate')) diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 0b6c646801aee..0286b387c35a5 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -64,9 +64,9 @@ export class AlbumService extends BaseService { return { ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - startDate: albumMetadata[album.id].startDate ?? undefined, - endDate: albumMetadata[album.id].endDate ?? undefined, - assetCount: albumMetadata[album.id].assetCount, + startDate: albumMetadata[album.id]?.startDate ?? undefined, + endDate: albumMetadata[album.id]?.endDate ?? undefined, + assetCount: albumMetadata[album.id]?.assetCount ?? 0, lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, }; }), @@ -83,9 +83,9 @@ export class AlbumService extends BaseService { return { ...mapAlbum(album, withAssets, auth), - startDate: albumMetadataForIds.startDate ?? undefined, - endDate: albumMetadataForIds.endDate ?? undefined, - assetCount: albumMetadataForIds.assetCount, + startDate: albumMetadataForIds?.startDate ?? undefined, + endDate: albumMetadataForIds?.endDate ?? undefined, + assetCount: albumMetadataForIds?.assetCount ?? 0, lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt, }; } From e5794e6cfcb97c120e1bed981c8110ff83f685a1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:44:12 +0000 Subject: [PATCH 159/184] chore: version v1.125.4 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 6fcbbcc5c957a..75e20968d6b7b 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.44", + "version": "2.2.45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.44", + "version": "2.2.45", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index e90f48a2e21a4..cb966cd431c69 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.44", + "version": "2.2.45", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 422ce6a8fbe39..13927be52772f 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.4", + "url": "https://v1.125.4.archive.immich.app" + }, { "label": "v1.125.3", "url": "https://v1.125.3.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 726dc58adc6d8..6beef479f2d59 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.3", + "version": "1.125.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.44", + "version": "2.2.45", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index ad19e4d0599d3..9e14d540c9a16 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.3", + "version": "1.125.4", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 9c0600bc1f057..96e23e829c9f5 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.3" +version = "1.125.4" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 824fdd9a97378..f19b8a8be21eb 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 179, - "android.injected.version.name" => "1.125.3", + "android.injected.version.code" => 180, + "android.injected.version.name" => "1.125.4", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 4c86002f5c410..aa53e7def4cc3 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.3" + version_number: "1.125.4" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e0a88fcb43b4a..d5e3889b4ebc7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.125.3 +- API version: 1.125.4 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 29bbc862473e3..3b05d6257b433 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.3+179 +version: 1.125.4+180 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 815dc7452dc34..80eebddf963c3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.3", + "version": "1.125.4", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 3ec67cee5fbc9..50a886bfbff85 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 7e1b6d66991ed..b04f9fd255ec8 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 3cfa15268fa1a..c466d71e87162 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.125.3 + * 1.125.4 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 10c21a8c04392..f348344ae3090 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.3", + "version": "1.125.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 5356b4c11b135..5fbe97caab2ff 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.3", + "version": "1.125.4", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 491ac70e853e8..fc360850f2ff0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.3", + "version": "1.125.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 861301ea0abdc..3a9d50653f191 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.3", + "version": "1.125.4", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 0fe62298e1f28bfa970e183a306b42d4036feafa Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Jan 2025 13:53:59 -0600 Subject: [PATCH 160/184] fix(server): duplicate detection (#15727) --- server/src/entities/asset.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index b7d3e7d4ab98d..e9dbe67a2fffc 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -193,7 +193,7 @@ export function withExifInner(qb: SelectQueryBuilder) { export function withSmartSearch(qb: SelectQueryBuilder) { return qb .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') - .select(sql`smart_search.embedding`.as('embedding')); + .select((eb) => eb.fn.toJson(eb.table('smart_search')).as('smartSearch')); } export function withFaces(eb: ExpressionBuilder) { From c139e05170dc039addd6c6894179208f48fe62d9 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Jan 2025 14:02:23 -0600 Subject: [PATCH 161/184] fix(mobile): locale option causes the datetime filter error out (#15704) --- mobile/lib/pages/search/search.page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 32e73f5c247cb..88cc56a14590d 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -277,7 +277,6 @@ class SearchPage extends HookConsumerWidget { fieldEndHintText: 'end_date'.tr(), initialEntryMode: DatePickerEntryMode.calendar, keyboardType: TextInputType.text, - locale: context.locale, ); if (date == null) { From 64d926581ffb933c45f6220961c56bb592205521 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:04:50 +0000 Subject: [PATCH 162/184] chore: version v1.125.5 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 75e20968d6b7b..9044ab7171ab1 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.45", + "version": "2.2.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.45", + "version": "2.2.46", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index cb966cd431c69..24da3c87ac9c9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.45", + "version": "2.2.46", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 13927be52772f..177f445f53b4d 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.5", + "url": "https://v1.125.5.archive.immich.app" + }, { "label": "v1.125.4", "url": "https://v1.125.4.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6beef479f2d59..5fd97d8f98f47 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.4", + "version": "1.125.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.45", + "version": "2.2.46", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 9e14d540c9a16..a65545ddb393c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.4", + "version": "1.125.5", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 96e23e829c9f5..14be0f1947eb1 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.4" +version = "1.125.5" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index f19b8a8be21eb..c943ca8f209be 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 180, - "android.injected.version.name" => "1.125.4", + "android.injected.version.code" => 181, + "android.injected.version.name" => "1.125.5", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index aa53e7def4cc3..82694c98356ad 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.4" + version_number: "1.125.5" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d5e3889b4ebc7..d448416a180af 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.125.4 +- API version: 1.125.5 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3b05d6257b433..3bd660dac2fae 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.4+180 +version: 1.125.5+181 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 80eebddf963c3..30fa7316ec9b8 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.4", + "version": "1.125.5", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 50a886bfbff85..8ceb4077987b6 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index b04f9fd255ec8..88f35e2bfe7b6 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c466d71e87162..93c72a477b2bd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.125.4 + * 1.125.5 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index f348344ae3090..9a3587fa3ae4a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.4", + "version": "1.125.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index 5fbe97caab2ff..f9b55a1ac42d0 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.4", + "version": "1.125.5", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index fc360850f2ff0..d57126bb80f7a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.4", + "version": "1.125.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 3a9d50653f191..9e9a6bf7e2765 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.4", + "version": "1.125.5", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 92412ca2f70bd117b44b5c8c6d19a68d3ed8e03e Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:20:18 -0500 Subject: [PATCH 163/184] fix(server): person thumbnail generation always being queued (#15734) * fix person thumbnail generation always being queued * fix thumbhash comparison * fix mock --- server/src/repositories/asset.repository.ts | 1 - server/src/repositories/person.repository.ts | 3 +-- server/src/services/media.service.ts | 2 +- server/test/repositories/media.repository.mock.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index b39781209eade..1f9f8f997f83b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -495,7 +495,6 @@ export class AssetRepository implements IAssetRepository { .$if(property === WithoutProperty.THUMBNAIL, (qb) => qb .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id') - .select(withFiles) .where('assets.isVisible', '=', true) .where((eb) => eb.or([ diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 45183f39d6fae..7c2512aa26f15 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -100,7 +100,6 @@ export class PersonRepository implements IPersonRepository { .$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!)) .$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!)) .$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!)) - .$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!)) .stream() as AsyncIterableIterator; } @@ -109,7 +108,7 @@ export class PersonRepository implements IPersonRepository { .selectFrom('person') .selectAll('person') .$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!)) - .$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!)) + .$if(options.thumbnailPath !== undefined, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!)) .$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null)) .$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!)) .$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!)) diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 5555a937f8387..c22d124b63400 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -194,7 +194,7 @@ export class MediaService extends BaseService { await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path))); } - if (asset.thumbhash != generated.thumbhash) { + if (!asset.thumbhash || Buffer.compare(asset.thumbhash, generated.thumbhash) !== 0) { await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash }); } diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 1e909dcae31bb..238066ad9e0ad 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -4,7 +4,7 @@ import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { return { generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()), - generateThumbhash: vitest.fn().mockImplementation(() => Promise.resolve()), + generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')), decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }), extract: vitest.fn().mockResolvedValue(false), probe: vitest.fn(), From f44669447fc2e84202b4eaeefa642b0c81de10a2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 02:58:27 +0000 Subject: [PATCH 164/184] chore: version v1.125.6 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 9044ab7171ab1..4e956fdfde0fc 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.46", + "version": "2.2.47", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.46", + "version": "2.2.47", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 24da3c87ac9c9..f356c7fbe70a6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.46", + "version": "2.2.47", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 177f445f53b4d..829935d60b4fc 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.125.6", + "url": "https://v1.125.6.archive.immich.app" + }, { "label": "v1.125.5", "url": "https://v1.125.5.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 5fd97d8f98f47..76314d99cc04d 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.125.5", + "version": "1.125.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.46", + "version": "2.2.47", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index a65545ddb393c..87193b55a0ce4 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.125.5", + "version": "1.125.6", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 14be0f1947eb1..c644caac71ca0 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.125.5" +version = "1.125.6" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index c943ca8f209be..b1dcfcb93856c 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 181, - "android.injected.version.name" => "1.125.5", + "android.injected.version.code" => 182, + "android.injected.version.name" => "1.125.6", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 82694c98356ad..52f6bc0fbe66c 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.125.5" + version_number: "1.125.6" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d448416a180af..01ced65598c80 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.125.5 +- API version: 1.125.6 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3bd660dac2fae..9741ddfa3e722 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.125.5+181 +version: 1.125.6+182 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 30fa7316ec9b8..fc62b58290b43 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7454,7 +7454,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.125.5", + "version": "1.125.6", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 8ceb4077987b6..62fe913e707a5 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 88f35e2bfe7b6..81352dc721de8 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 93c72a477b2bd..088e30f9d8b4f 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.125.5 + * 1.125.6 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 9a3587fa3ae4a..ab9c5f91a0c34 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.125.5", + "version": "1.125.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.0", diff --git a/server/package.json b/server/package.json index f9b55a1ac42d0..974256c8e30f2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.125.5", + "version": "1.125.6", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index d57126bb80f7a..c445a58b97be4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.125.5", + "version": "1.125.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -75,7 +75,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 9e9a6bf7e2765..de60c948873b3 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.125.5", + "version": "1.125.6", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From fe1e09e51f565b0e521862b5f3e18a8b5b1f588a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=BCndig?= Date: Tue, 28 Jan 2025 04:54:29 +0100 Subject: [PATCH 165/184] fix(server): Allow negative rating (for rejected images) (#15699) Allow negative rating (for rejected images) --- e2e/src/api/specs/asset.e2e-spec.ts | 14 ++++++++++++++ .../openapi/lib/model/asset_bulk_update_dto.dart | 2 +- mobile/openapi/lib/model/update_asset_dto.dart | 2 +- open-api/immich-openapi-specs.json | 4 ++-- server/src/dtos/asset.dto.ts | 2 +- server/src/services/metadata.service.spec.ts | 11 +++++++++++ server/src/services/metadata.service.ts | 2 +- 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 32cbdd6df812a..1b644454aab0b 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -701,6 +701,20 @@ describe('/asset', () => { expect(status).toEqual(200); }); + it('should set the negative rating', async () => { + const { status, body } = await request(app) + .put(`/assets/${user1Assets[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ rating: -1 }); + expect(body).toMatchObject({ + id: user1Assets[0].id, + exifInfo: expect.objectContaining({ + rating: -1, + }), + }); + expect(status).toEqual(200); + }); + it('should reject invalid rating', async () => { for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { const { status, body } = await request(app) diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index da23d2f09d2e0..0b5a2c30d913b 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -67,7 +67,7 @@ class AssetBulkUpdateDto { /// num? longitude; - /// Minimum value: 0 + /// Minimum value: -1 /// Maximum value: 5 /// /// Please note: This property should have been non-nullable! Since the specification file diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 9ebce5fd9232b..c6ae6d8e07d3d 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -73,7 +73,7 @@ class UpdateAssetDto { /// num? longitude; - /// Minimum value: 0 + /// Minimum value: -1 /// Maximum value: 5 /// /// Please note: This property should have been non-nullable! Since the specification file diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index fc62b58290b43..3067b25449482 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7951,7 +7951,7 @@ }, "rating": { "maximum": 5, - "minimum": 0, + "minimum": -1, "type": "number" } }, @@ -12780,7 +12780,7 @@ }, "rating": { "maximum": 5, - "minimum": 0, + "minimum": -1, "type": "number" } }, diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 42d6d7d7451eb..8aa63f2f6924c 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -52,7 +52,7 @@ export class UpdateAssetBase { @Optional() @IsInt() @Max(5) - @Min(0) + @Min(-1) rating?: number; } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 8cc6e014d2038..99ca1e7ed3120 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1162,6 +1162,17 @@ describe(MetadataService.name, () => { }), ); }); + it('should handle valid negative rating value', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + mockReadTags({ Rating: -1 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: -1, + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index d5b7e6e4e4e8b..db3af9fca09e3 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -204,7 +204,7 @@ export class MetadataService extends BaseService { // comments description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), profileDescription: exifTags.ProfileDescription || null, - rating: validateRange(exifTags.Rating, 0, 5), + rating: validateRange(exifTags.Rating, -1, 5), // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, From 92dff839d091ac837cca903fb521fd0dae4ee22d Mon Sep 17 00:00:00 2001 From: RiggiG <44820045+RiggiG@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:54:56 -0500 Subject: [PATCH 166/184] fix(web): do not throw error when hash fails (#15740) change: do not throw error when hash fails --- web/src/lib/utils/file-uploader.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 407501e6229f1..04d4d788100e9 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -157,7 +157,6 @@ async function fileUploader( } } catch (error) { console.error(`Error calculating sha1 file=${assetFile.name})`, error); - throw error; } } From 08db77db231420849ef5f4c082d705838d04e19a Mon Sep 17 00:00:00 2001 From: PastLeo Date: Tue, 28 Jan 2025 23:09:40 +0800 Subject: [PATCH 167/184] =?UTF-8?q?feat:=20resolution=20selection=20and=20?= =?UTF-8?q?default=20preview=20playback=20for=20360=C2=B0=20panorama=20vid?= =?UTF-8?q?eos=20(#15747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * original/preview switching in photo-sphere-viewer 1. default to preview in photo-sphere-viewer video mode 2. install and integrate @photo-sphere-viewer/settings-plugin & @photo-sphere-viewer/resolution-plugin * fix lint errors --- web/package-lock.json | 21 ++++++++++ web/package.json | 2 + .../asset-viewer/image-panorama-viewer.svelte | 2 +- .../photo-sphere-viewer-adapter.svelte | 42 +++++++++++++++---- .../asset-viewer/video-panorama-viewer.svelte | 10 ++++- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index c445a58b97be4..b6454c2caae99 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,8 @@ "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", + "@photo-sphere-viewer/resolution-plugin": "^5.11.5", + "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", "dom-to-image": "^2.6.0", @@ -1669,6 +1671,25 @@ "@photo-sphere-viewer/video-plugin": "5.11.5" } }, + "node_modules/@photo-sphere-viewer/resolution-plugin": { + "version": "5.11.5", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.11.5.tgz", + "integrity": "sha512-Dbvp5bBtozD3IWt1Q0wORVaZBcB1bV9xUeoOS9A7F7b3EkQ2pkC5/jot/1AyM4wtU5wJ63NWHskQ1d7m6WWazQ==", + "license": "MIT", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.11.5", + "@photo-sphere-viewer/settings-plugin": "5.11.5" + } + }, + "node_modules/@photo-sphere-viewer/settings-plugin": { + "version": "5.11.5", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.11.5.tgz", + "integrity": "sha512-ZgYaWjiBMhsoRH5ddW3h+v4J4LPmofsT7BBRq5UCssWw2Fsrvv7mFFRi4UbZ1qzeKmvNUOr8BaFQgX1ZLvUWfQ==", + "license": "MIT", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.11.5" + } + }, "node_modules/@photo-sphere-viewer/video-plugin": { "version": "5.11.5", "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.5.tgz", diff --git a/web/package.json b/web/package.json index de60c948873b3..9c9bfc680c896 100644 --- a/web/package.json +++ b/web/package.json @@ -72,6 +72,8 @@ "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", + "@photo-sphere-viewer/resolution-plugin": "^5.11.5", + "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", "dom-to-image": "^2.6.0", diff --git a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte index 6da8cc33d3bc9..7b9fd85b4a2dc 100644 --- a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte @@ -24,7 +24,7 @@ {:then [data, { default: PhotoSphereViewer }]} {:catch} {$t('errors.failed_to_load_asset')} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 0c8f76a01ed7a..517e630dc91d3 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -7,18 +7,21 @@ type AdapterConstructor, type PluginConstructor, } from '@photo-sphere-viewer/core'; + import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin'; + import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin'; import '@photo-sphere-viewer/core/index.css'; + import '@photo-sphere-viewer/settings-plugin/index.css'; import { onDestroy, onMount } from 'svelte'; interface Props { panorama: string | { source: string }; - originalImageUrl?: string; + originalPanorama?: string | { source: string }; adapter?: AdapterConstructor | [AdapterConstructor, unknown]; plugins?: (PluginConstructor | [PluginConstructor, unknown])[]; navbar?: boolean; } - let { panorama, originalImageUrl, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props(); + let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props(); let container: HTMLDivElement | undefined = $state(); let viewer: Viewer; @@ -30,9 +33,33 @@ viewer = new Viewer({ adapter, - plugins, + plugins: [ + SettingsPlugin, + [ + ResolutionPlugin, + { + defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default', + resolutions: [ + { + id: 'default', + label: 'Default', + panorama, + }, + ...(originalPanorama + ? [ + { + id: 'original', + label: 'Original', + panorama: originalPanorama, + }, + ] + : []), + ], + }, + ], + ...plugins, + ], container, - panorama, touchmoveTwoFingers: false, mousewheelCtrlKey: false, navbar, @@ -40,15 +67,14 @@ maxFov: 120, fisheye: false, }); + const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin; - if (originalImageUrl && !$alwaysLoadOriginalFile) { + if (originalPanorama && !$alwaysLoadOriginalFile) { const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => { // zoomLevel range: [0, 100] if (Math.round(zoomLevel) >= 75) { // Replace the preview with the original - viewer.setPanorama(originalImageUrl, { showLoader: false, speed: 150 }).catch(() => { - viewer.setPanorama(panorama, { showLoader: false, speed: 0 }).catch(() => {}); - }); + void resolutionPlugin.setResolution('original'); viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler); } }; diff --git a/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte index 73315d661ed49..a205ffce3cf5a 100644 --- a/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte @@ -1,5 +1,5 @@ - {getDateRange(startDate, endDate)} + {getAlbumDateRange(album)} {$t('items_count', { values: { count: album.assetCount } })} diff --git a/web/src/lib/utils/date-time.spec.ts b/web/src/lib/utils/date-time.spec.ts index cbb08418c0cbf..90db980e2a01a 100644 --- a/web/src/lib/utils/date-time.spec.ts +++ b/web/src/lib/utils/date-time.spec.ts @@ -1,4 +1,5 @@ -import { timeToSeconds } from './date-time'; +import { writable } from 'svelte/store'; +import { getAlbumDateRange, timeToSeconds } from './date-time'; describe('converting time to seconds', () => { it('parses hh:mm:ss correctly', () => { @@ -21,3 +22,30 @@ describe('converting time to seconds', () => { expect(timeToSeconds('01:02:03.456.123456')).toBeCloseTo(3723.456); }); }); + +describe('getAlbumDate', () => { + beforeAll(() => { + process.env.TZ = 'UTC'; + + vitest.mock('$lib/stores/preferences.store', () => ({ + locale: writable('en'), + })); + }); + + it('should work with only a start date', () => { + expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00Z' })).toEqual('Jan 1, 2021'); + }); + + it('should work with a start and end date', () => { + expect( + getAlbumDateRange({ + startDate: '2021-01-01T00:00:00Z', + endDate: '2021-01-05T00:00:00Z', + }), + ).toEqual('Jan 1, 2021 - Jan 5, 2021'); + }); + + it('should work with the new date format', () => { + expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00+05:00' })).toEqual('Jan 1, 2021'); + }); +}); diff --git a/web/src/lib/utils/date-time.ts b/web/src/lib/utils/date-time.ts index d5482f153ff06..ba22503c7086e 100644 --- a/web/src/lib/utils/date-time.ts +++ b/web/src/lib/utils/date-time.ts @@ -1,3 +1,4 @@ +import { dateFormats } from '$lib/constants'; import { locale } from '$lib/stores/preferences.store'; import { DateTime, Duration } from 'luxon'; import { get } from 'svelte/store'; @@ -51,3 +52,28 @@ export const getShortDateRange = (startDate: string | Date, endDate: string | Da return `${startDateLocalized} - ${endDateLocalized}`; } }; + +const formatDate = (date?: string) => { + if (!date) { + return; + } + + // without timezone + const localDate = date.replace(/Z$/, '').replace(/\+.+$/, ''); + return localDate ? new Date(localDate).toLocaleDateString(get(locale), dateFormats.album) : undefined; +}; + +export const getAlbumDateRange = (album: { startDate?: string; endDate?: string }) => { + const start = formatDate(album.startDate); + const end = formatDate(album.endDate); + + if (start && end && start !== end) { + return `${start} - ${end}`; + } + + if (start) { + return start; + } + + return ''; +}; From 4fccc09fc128e88c0485691b6b544b1a4d027ada Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Fri, 31 Jan 2025 03:34:12 +0100 Subject: [PATCH 184/184] chore: fix typo in libraries.md (#15800) Fix typo in libraries.md --- docs/docs/features/libraries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 6a1dba9ebaab2..796337f37c429 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -58,7 +58,7 @@ If your photos are on a network drive, automatic file watching likely won't work #### Troubleshooting -If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files. +If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watches` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files. ``` ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'